summaryrefslogtreecommitdiffstats
path: root/src/udev
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
commit2cb7e0aaedad73b076ea18c6900b0e86c5760d79 (patch)
treeda68ca54bb79f4080079bf0828acda937593a4e1 /src/udev
parentInitial commit. (diff)
downloadsystemd-2cb7e0aaedad73b076ea18c6900b0e86c5760d79.tar.xz
systemd-2cb7e0aaedad73b076ea18c6900b0e86c5760d79.zip
Adding upstream version 247.3.upstream/247.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/udev')
-rw-r--r--src/udev/.vimrc4
-rw-r--r--src/udev/ata_id/ata_id.c651
-rw-r--r--src/udev/cdrom_id/cdrom_id.c1018
-rw-r--r--src/udev/fido_id/fido_id.c96
-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.c23
-rw-r--r--src/udev/fido_id/test-fido-id-desc.c85
-rwxr-xr-xsrc/udev/generate-keyboard-keys-gperf.sh18
-rwxr-xr-xsrc/udev/generate-keyboard-keys-list.sh7
-rw-r--r--src/udev/meson.build225
-rw-r--r--src/udev/mtd_probe/mtd_probe.c59
-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.c28
-rw-r--r--src/udev/net/fuzz-link-parser.options2
-rw-r--r--src/udev/net/link-config-gperf.gperf68
-rw-r--r--src/udev/net/link-config.c711
-rw-r--r--src/udev/net/link-config.h98
-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.c595
-rw-r--r--src/udev/scsi_id/scsi_id.h63
-rw-r--r--src/udev/scsi_id/scsi_serial.c893
-rw-r--r--src/udev/udev-builtin-blkid.c317
-rw-r--r--src/udev/udev-builtin-btrfs.c40
-rw-r--r--src/udev/udev-builtin-hwdb.c221
-rw-r--r--src/udev/udev-builtin-input_id.c395
-rw-r--r--src/udev/udev-builtin-keyboard.c254
-rw-r--r--src/udev/udev-builtin-kmod.c76
-rw-r--r--src/udev/udev-builtin-net_id.c961
-rw-r--r--src/udev/udev-builtin-net_setup_link.c88
-rw-r--r--src/udev/udev-builtin-path_id.c732
-rw-r--r--src/udev/udev-builtin-uaccess.c80
-rw-r--r--src/udev/udev-builtin-usb_id.c462
-rw-r--r--src/udev/udev-builtin.c145
-rw-r--r--src/udev/udev-builtin.h71
-rw-r--r--src/udev/udev-ctrl.c393
-rw-r--r--src/udev/udev-ctrl.h79
-rw-r--r--src/udev/udev-event.c1087
-rw-r--r--src/udev/udev-event.h69
-rw-r--r--src/udev/udev-node.c528
-rw-r--r--src/udev/udev-node.h15
-rw-r--r--src/udev/udev-rules.c2393
-rw-r--r--src/udev/udev-rules.h30
-rw-r--r--src/udev/udev-watch.c174
-rw-r--r--src/udev/udev-watch.h10
-rw-r--r--src/udev/udev.conf11
-rw-r--r--src/udev/udev.pc.in6
-rw-r--r--src/udev/udevadm-control.c186
-rw-r--r--src/udev/udevadm-hwdb.c101
-rw-r--r--src/udev/udevadm-info.c519
-rw-r--r--src/udev/udevadm-monitor.c254
-rw-r--r--src/udev/udevadm-settle.c226
-rw-r--r--src/udev/udevadm-test-builtin.c97
-rw-r--r--src/udev/udevadm-test.c162
-rw-r--r--src/udev/udevadm-trigger.c393
-rw-r--r--src/udev/udevadm-util.c95
-rw-r--r--src/udev/udevadm-util.h6
-rw-r--r--src/udev/udevadm.c134
-rw-r--r--src/udev/udevadm.h22
-rw-r--r--src/udev/udevd.c1960
-rw-r--r--src/udev/udevd.h4
-rw-r--r--src/udev/v4l_id/v4l_id.c93
64 files changed, 17886 insertions, 0 deletions
diff --git a/src/udev/.vimrc b/src/udev/.vimrc
new file mode 100644
index 0000000..366fbdc
--- /dev/null
+++ b/src/udev/.vimrc
@@ -0,0 +1,4 @@
+" 'set exrc' in ~/.vimrc will read .vimrc from the current directory
+set tabstop=8
+set shiftwidth=8
+set expandtab
diff --git a/src/udev/ata_id/ata_id.c b/src/udev/ata_id/ata_id.c
new file mode 100644
index 0000000..ce0bf5d
--- /dev/null
+++ b/src/udev/ata_id/ata_id.c
@@ -0,0 +1,651 @@
+/* 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 "fd-util.h"
+#include "libudev-util.h"
+#include "log.h"
+#include "memory-util.h"
+#include "udev-util.h"
+
+#define COMMAND_TIMEOUT_MSEC (30 * 1000)
+
+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,
+ };
+ int ret;
+
+ ret = ioctl(fd, SG_IO, &io_v4);
+ if (ret != 0) {
+ /* could be that the driver doesn't do version 4, try version 3 */
+ if (errno == EINVAL) {
+ 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,
+ };
+
+ ret = ioctl(fd, SG_IO, &io_hdr);
+ if (ret != 0)
+ return ret;
+
+ /* 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)) {
+ errno = EIO;
+ return -1;
+ }
+ } else
+ return ret;
+ }
+
+ /* 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)) {
+ errno = EIO;
+ return -1;
+ }
+
+ 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,
+ };
+ int ret;
+
+ ret = ioctl(fd, SG_IO, &io_v4);
+ if (ret != 0) {
+ /* could be that the driver doesn't do version 4, try version 3 */
+ if (errno == EINVAL) {
+ 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,
+ };
+
+ ret = ioctl(fd, SG_IO, &io_hdr);
+ if (ret != 0)
+ return ret;
+ } else
+ return ret;
+ }
+
+ if (!(sense[0] == 0x72 && desc[0] == 0x9 && desc[1] == 0x0c) &&
+ !(sense[0] == 0x70 && sense[12] == 0x00 && sense[13] == 0x1d)) {
+ errno = EIO;
+ return -1;
+ }
+
+ 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,
+ };
+ int ret;
+
+ ret = ioctl(fd, SG_IO, &io_v4);
+ if (ret != 0) {
+ /* could be that the driver doesn't do version 4, try version 3 */
+ if (errno == EINVAL) {
+ 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,
+ };
+
+ ret = ioctl(fd, SG_IO, &io_hdr);
+ if (ret != 0)
+ return ret;
+ } else
+ return ret;
+ }
+
+ if (!(sense[0] == 0x72 && desc[0] == 0x9 && desc[1] == 0x0c)) {
+ errno = EIO;
+ return -1;
+ }
+
+ 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) {
+ 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) {
+ uint16_t *p;
+
+ p = (uint16_t *) identify;
+ p[offset_words] = le16toh (p[offset_words]);
+}
+
+/**
+ * disk_identify:
+ * @fd: File descriptor for the block device.
+ * @out_identify: Return location for IDENTIFY data.
+ * @out_is_packet_device: Return location for whether returned data is from a IDENTIFY PACKET DEVICE.
+ *
+ * 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 and @out_is_packet_device.
+ *
+ * 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 *out_is_packet_device) {
+ int ret;
+ uint8_t inquiry_buf[36];
+ int peripheral_device_type;
+ int all_nul_bytes;
+ int n;
+ int is_packet_device = 0;
+
+ /* 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.)
+ */
+ ret = disk_scsi_inquiry_command (fd, inquiry_buf, sizeof (inquiry_buf));
+ if (ret != 0)
+ goto out;
+
+ /* SPC-4, section 6.4.2: Standard INQUIRY data */
+ peripheral_device_type = inquiry_buf[0] & 0x1f;
+ if (peripheral_device_type == 0x05)
+ {
+ is_packet_device = 1;
+ ret = disk_identify_packet_device_command(fd, out_identify, 512);
+ goto check_nul_bytes;
+ }
+ if (!IN_SET(peripheral_device_type, 0x00, 0x14)) {
+ ret = -1;
+ errno = EIO;
+ goto out;
+ }
+
+ /* OK, now issue the IDENTIFY DEVICE command */
+ ret = disk_identify_command(fd, out_identify, 512);
+ if (ret != 0)
+ goto out;
+
+ check_nul_bytes:
+ /* Check if IDENTIFY data is all NUL bytes - if so, bail */
+ all_nul_bytes = 1;
+ for (n = 0; n < 512; n++) {
+ if (out_identify[n] != '\0') {
+ all_nul_bytes = 0;
+ break;
+ }
+ }
+
+ if (all_nul_bytes) {
+ ret = -1;
+ errno = EIO;
+ goto out;
+ }
+
+out:
+ if (out_is_packet_device)
+ *out_is_packet_device = is_packet_device;
+ return ret;
+}
+
+int main(int argc, char *argv[]) {
+ struct hd_driveid id;
+ union {
+ uint8_t byte[512];
+ uint16_t wyde[256];
+ } identify;
+ char model[41];
+ char model_enc[256];
+ char serial[21];
+ char revision[9];
+ const char *node = NULL;
+ int export = 0;
+ _cleanup_close_ int fd = -1;
+ uint16_t word;
+ int is_packet_device = 0;
+ static const struct option options[] = {
+ { "export", no_argument, NULL, 'x' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ for (;;) {
+ int option;
+
+ option = getopt_long(argc, argv, "xh", options, NULL);
+ if (option == -1)
+ break;
+
+ switch (option) {
+ case 'x':
+ export = 1;
+ break;
+ case 'h':
+ printf("Usage: %s [--export] [--help] <device>\n"
+ " -x,--export print values as environment keys\n"
+ " -h,--help print this help text\n\n",
+ program_invocation_short_name);
+ return 0;
+ }
+ }
+
+ node = argv[optind];
+ if (!node) {
+ log_error("no node specified");
+ return 1;
+ }
+
+ fd = open(node, O_RDONLY|O_NONBLOCK|O_CLOEXEC);
+ if (fd < 0) {
+ log_error("unable to open '%s'", node);
+ return 1;
+ }
+
+ if (disk_identify(fd, identify.byte, &is_packet_device) == 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) {
+ log_debug_errno(errno, "HDIO_GET_IDENTITY failed for '%s': %m", node);
+ return 2;
+ }
+ }
+
+ memcpy(model, id.model, 40);
+ model[40] = '\0';
+ udev_util_encode_string(model, model_enc, sizeof(model_enc));
+ util_replace_whitespace((char *) id.model, model, 40);
+ util_replace_chars(model, NULL);
+ util_replace_whitespace((char *) id.serial_no, serial, 20);
+ util_replace_chars(serial, NULL);
+ util_replace_whitespace((char *) id.fw_rev, revision, 8);
+ util_replace_chars(revision, NULL);
+
+ if (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");
+ } else {
+ if (serial[0] != '\0')
+ printf("%s_%s\n", model, serial);
+ else
+ printf("%s\n", model);
+ }
+
+ return 0;
+}
diff --git a/src/udev/cdrom_id/cdrom_id.c b/src/udev/cdrom_id/cdrom_id.c
new file mode 100644
index 0000000..804cc7c
--- /dev/null
+++ b/src/udev/cdrom_id/cdrom_id.c
@@ -0,0 +1,1018 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * cdrom_id - optical drive and media information prober
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <linux/cdrom.h>
+#include <scsi/sg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "udev-util.h"
+
+/* device info */
+static unsigned cd_cd_rom;
+static unsigned cd_cd_r;
+static unsigned cd_cd_rw;
+static unsigned cd_dvd_rom;
+static unsigned cd_dvd_r;
+static unsigned cd_dvd_rw;
+static unsigned cd_dvd_ram;
+static unsigned cd_dvd_plus_r;
+static unsigned cd_dvd_plus_rw;
+static unsigned cd_dvd_plus_r_dl;
+static unsigned cd_dvd_plus_rw_dl;
+static unsigned cd_bd;
+static unsigned cd_bd_r;
+static unsigned cd_bd_re;
+static unsigned cd_hddvd;
+static unsigned cd_hddvd_r;
+static unsigned cd_hddvd_rw;
+static unsigned cd_mo;
+static unsigned cd_mrw;
+static unsigned cd_mrw_w;
+
+/* media info */
+static unsigned cd_media;
+static unsigned cd_media_cd_rom;
+static unsigned cd_media_cd_r;
+static unsigned cd_media_cd_rw;
+static unsigned cd_media_dvd_rom;
+static unsigned cd_media_dvd_r;
+static unsigned cd_media_dvd_rw;
+static unsigned cd_media_dvd_rw_ro; /* restricted overwrite mode */
+static unsigned cd_media_dvd_rw_seq; /* sequential mode */
+static unsigned cd_media_dvd_ram;
+static unsigned cd_media_dvd_plus_r;
+static unsigned cd_media_dvd_plus_rw;
+static unsigned cd_media_dvd_plus_r_dl;
+static unsigned cd_media_dvd_plus_rw_dl;
+static unsigned cd_media_bd;
+static unsigned cd_media_bd_r;
+static unsigned cd_media_bd_re;
+static unsigned cd_media_hddvd;
+static unsigned cd_media_hddvd_r;
+static unsigned cd_media_hddvd_rw;
+static unsigned cd_media_mo;
+static unsigned cd_media_mrw;
+static unsigned cd_media_mrw_w;
+
+static const char *cd_media_state = NULL;
+static unsigned cd_media_session_next;
+static unsigned cd_media_session_count;
+static unsigned cd_media_track_count;
+static unsigned cd_media_track_count_data;
+static unsigned cd_media_track_count_audio;
+static unsigned long long int cd_media_session_last_offset;
+
+#define ERRCODE(s) ((((s)[2] & 0x0F) << 16) | ((s)[12] << 8) | ((s)[13]))
+#define SK(errcode) (((errcode) >> 16) & 0xF)
+#define ASC(errcode) (((errcode) >> 8) & 0xFF)
+#define ASCQ(errcode) ((errcode) & 0xFF)
+
+static void info_scsi_cmd_err(const char *cmd, int err) {
+ if (err == -1)
+ log_debug("%s failed", cmd);
+ else
+ log_debug("%s failed with SK=%Xh/ASC=%02Xh/ACQ=%02Xh", cmd, SK(err), ASC(err), ASCQ(err));
+}
+
+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;
+}
+
+#define CHECK_CONDITION 0x01
+
+static int scsi_cmd_run(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize) {
+ int ret = 0;
+
+ 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))
+ return -1;
+
+ if ((cmd->sg_io.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
+ errno = EIO;
+ ret = -1;
+ if (cmd->sg_io.masked_status & CHECK_CONDITION) {
+ ret = ERRCODE(cmd->_sense.u);
+ if (ret == 0)
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static int media_lock(int fd, bool lock) {
+ int err;
+
+ /* disable the kernel's lock logic */
+ err = ioctl(fd, CDROM_CLEAR_OPTIONS, CDO_LOCK);
+ if (err < 0)
+ log_debug("CDROM_CLEAR_OPTIONS, CDO_LOCK failed");
+
+ err = ioctl(fd, CDROM_LOCKDOOR, lock ? 1 : 0);
+ if (err < 0)
+ log_debug("CDROM_LOCKDOOR failed");
+
+ return err;
+}
+
+static int media_eject(int fd) {
+ struct scsi_cmd sc;
+ int err;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x1b);
+ scsi_cmd_set(&sc, 4, 0x02);
+ scsi_cmd_set(&sc, 5, 0);
+ err = scsi_cmd_run(&sc, fd, NULL, 0);
+ if (err != 0) {
+ info_scsi_cmd_err("START_STOP_UNIT", err);
+ return -1;
+ }
+ return 0;
+}
+
+static int cd_capability_compat(int fd) {
+ int capability;
+
+ capability = ioctl(fd, CDROM_GET_CAPABILITY, NULL);
+ if (capability < 0)
+ return log_debug_errno(errno, "CDROM_GET_CAPABILITY failed");
+
+ if (capability & CDC_CD_R)
+ cd_cd_r = 1;
+ if (capability & CDC_CD_RW)
+ cd_cd_rw = 1;
+ if (capability & CDC_DVD)
+ cd_dvd_rom = 1;
+ if (capability & CDC_DVD_R)
+ cd_dvd_r = 1;
+ if (capability & CDC_DVD_RAM)
+ cd_dvd_ram = 1;
+ if (capability & CDC_MRW)
+ cd_mrw = 1;
+ if (capability & CDC_MRW_W)
+ cd_mrw_w = 1;
+ return 0;
+}
+
+static int cd_media_compat(int fd) {
+ if (ioctl(fd, CDROM_DRIVE_STATUS, CDSL_CURRENT) != CDS_DISC_OK)
+ return log_debug_errno(errno, "CDROM_DRIVE_STATUS != CDS_DISC_OK");
+
+ cd_media = 1;
+ return 0;
+}
+
+static int cd_inquiry(int fd) {
+ struct scsi_cmd sc;
+ unsigned char inq[128];
+ int err;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x12);
+ scsi_cmd_set(&sc, 4, 36);
+ scsi_cmd_set(&sc, 5, 0);
+ err = scsi_cmd_run(&sc, fd, inq, 36);
+ if (err != 0) {
+ info_scsi_cmd_err("INQUIRY", err);
+ return -1;
+ }
+
+ 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 void feature_profile_media(int cur_profile) {
+ switch (cur_profile) {
+ case 0x03:
+ case 0x04:
+ case 0x05:
+ log_debug("profile 0x%02x ", cur_profile);
+ cd_media = 1;
+ cd_media_mo = 1;
+ break;
+ case 0x08:
+ log_debug("profile 0x%02x media_cd_rom", cur_profile);
+ cd_media = 1;
+ cd_media_cd_rom = 1;
+ break;
+ case 0x09:
+ log_debug("profile 0x%02x media_cd_r", cur_profile);
+ cd_media = 1;
+ cd_media_cd_r = 1;
+ break;
+ case 0x0a:
+ log_debug("profile 0x%02x media_cd_rw", cur_profile);
+ cd_media = 1;
+ cd_media_cd_rw = 1;
+ break;
+ case 0x10:
+ log_debug("profile 0x%02x media_dvd_ro", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_rom = 1;
+ break;
+ case 0x11:
+ log_debug("profile 0x%02x media_dvd_r", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_r = 1;
+ break;
+ case 0x12:
+ log_debug("profile 0x%02x media_dvd_ram", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_ram = 1;
+ break;
+ case 0x13:
+ log_debug("profile 0x%02x media_dvd_rw_ro", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_rw = 1;
+ cd_media_dvd_rw_ro = 1;
+ break;
+ case 0x14:
+ log_debug("profile 0x%02x media_dvd_rw_seq", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_rw = 1;
+ cd_media_dvd_rw_seq = 1;
+ break;
+ case 0x1B:
+ log_debug("profile 0x%02x media_dvd_plus_r", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_plus_r = 1;
+ break;
+ case 0x1A:
+ log_debug("profile 0x%02x media_dvd_plus_rw", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_plus_rw = 1;
+ break;
+ case 0x2A:
+ log_debug("profile 0x%02x media_dvd_plus_rw_dl", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_plus_rw_dl = 1;
+ break;
+ case 0x2B:
+ log_debug("profile 0x%02x media_dvd_plus_r_dl", cur_profile);
+ cd_media = 1;
+ cd_media_dvd_plus_r_dl = 1;
+ break;
+ case 0x40:
+ log_debug("profile 0x%02x media_bd", cur_profile);
+ cd_media = 1;
+ cd_media_bd = 1;
+ break;
+ case 0x41:
+ case 0x42:
+ log_debug("profile 0x%02x media_bd_r", cur_profile);
+ cd_media = 1;
+ cd_media_bd_r = 1;
+ break;
+ case 0x43:
+ log_debug("profile 0x%02x media_bd_re", cur_profile);
+ cd_media = 1;
+ cd_media_bd_re = 1;
+ break;
+ case 0x50:
+ log_debug("profile 0x%02x media_hddvd", cur_profile);
+ cd_media = 1;
+ cd_media_hddvd = 1;
+ break;
+ case 0x51:
+ log_debug("profile 0x%02x media_hddvd_r", cur_profile);
+ cd_media = 1;
+ cd_media_hddvd_r = 1;
+ break;
+ case 0x52:
+ log_debug("profile 0x%02x media_hddvd_rw", cur_profile);
+ cd_media = 1;
+ cd_media_hddvd_rw = 1;
+ break;
+ default:
+ log_debug("profile 0x%02x <ignored>", cur_profile);
+ break;
+ }
+}
+
+static int feature_profiles(const unsigned char *profiles, size_t size) {
+ unsigned i;
+
+ for (i = 0; i+4 <= size; i += 4) {
+ int profile;
+
+ profile = profiles[i] << 8 | profiles[i+1];
+ switch (profile) {
+ case 0x03:
+ case 0x04:
+ case 0x05:
+ log_debug("profile 0x%02x mo", profile);
+ cd_mo = 1;
+ break;
+ case 0x08:
+ log_debug("profile 0x%02x cd_rom", profile);
+ cd_cd_rom = 1;
+ break;
+ case 0x09:
+ log_debug("profile 0x%02x cd_r", profile);
+ cd_cd_r = 1;
+ break;
+ case 0x0A:
+ log_debug("profile 0x%02x cd_rw", profile);
+ cd_cd_rw = 1;
+ break;
+ case 0x10:
+ log_debug("profile 0x%02x dvd_rom", profile);
+ cd_dvd_rom = 1;
+ break;
+ case 0x12:
+ log_debug("profile 0x%02x dvd_ram", profile);
+ cd_dvd_ram = 1;
+ break;
+ case 0x13:
+ case 0x14:
+ log_debug("profile 0x%02x dvd_rw", profile);
+ cd_dvd_rw = 1;
+ break;
+ case 0x1B:
+ log_debug("profile 0x%02x dvd_plus_r", profile);
+ cd_dvd_plus_r = 1;
+ break;
+ case 0x1A:
+ log_debug("profile 0x%02x dvd_plus_rw", profile);
+ cd_dvd_plus_rw = 1;
+ break;
+ case 0x2A:
+ log_debug("profile 0x%02x dvd_plus_rw_dl", profile);
+ cd_dvd_plus_rw_dl = 1;
+ break;
+ case 0x2B:
+ log_debug("profile 0x%02x dvd_plus_r_dl", profile);
+ cd_dvd_plus_r_dl = 1;
+ break;
+ case 0x40:
+ cd_bd = 1;
+ log_debug("profile 0x%02x bd", profile);
+ break;
+ case 0x41:
+ case 0x42:
+ cd_bd_r = 1;
+ log_debug("profile 0x%02x bd_r", profile);
+ break;
+ case 0x43:
+ cd_bd_re = 1;
+ log_debug("profile 0x%02x bd_re", profile);
+ break;
+ case 0x50:
+ cd_hddvd = 1;
+ log_debug("profile 0x%02x hddvd", profile);
+ break;
+ case 0x51:
+ cd_hddvd_r = 1;
+ log_debug("profile 0x%02x hddvd_r", profile);
+ break;
+ case 0x52:
+ cd_hddvd_rw = 1;
+ log_debug("profile 0x%02x hddvd_rw", profile);
+ break;
+ default:
+ log_debug("profile 0x%02x <ignored>", profile);
+ break;
+ }
+ }
+ return 0;
+}
+
+/* returns 0 if media was detected */
+static int cd_profiles_old_mmc(int fd) {
+ struct scsi_cmd sc;
+ int err;
+
+ unsigned char header[32];
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x51);
+ scsi_cmd_set(&sc, 8, sizeof(header));
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, header, sizeof(header));
+ if (err != 0) {
+ info_scsi_cmd_err("READ DISC INFORMATION", err);
+ if (cd_media == 1) {
+ log_debug("no current profile, but disc is present; assuming CD-ROM");
+ cd_media_cd_rom = 1;
+ cd_media_track_count = 1;
+ cd_media_track_count_data = 1;
+ return 0;
+ } else
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
+ "no current profile, assuming no media");
+ };
+
+ cd_media = 1;
+
+ if (header[2] & 16) {
+ cd_media_cd_rw = 1;
+ log_debug("profile 0x0a media_cd_rw");
+ } else if ((header[2] & 3) < 2 && cd_cd_r) {
+ cd_media_cd_r = 1;
+ log_debug("profile 0x09 media_cd_r");
+ } else {
+ cd_media_cd_rom = 1;
+ log_debug("profile 0x08 media_cd_rom");
+ }
+ return 0;
+}
+
+/* returns 0 if media was detected */
+static int cd_profiles(int fd) {
+ struct scsi_cmd sc;
+ unsigned char features[65530];
+ unsigned cur_profile = 0;
+ unsigned len;
+ unsigned i;
+ int err;
+ int ret;
+
+ ret = -1;
+
+ /* First query the current profile */
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x46);
+ scsi_cmd_set(&sc, 8, 8);
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, features, 8);
+ if (err != 0) {
+ info_scsi_cmd_err("GET CONFIGURATION", err);
+ /* handle pre-MMC2 drives which do not support GET CONFIGURATION */
+ if (SK(err) == 0x5 && IN_SET(ASC(err), 0x20, 0x24)) {
+ log_debug("drive is pre-MMC2 and does not support 46h get configuration command");
+ log_debug("trying to work around the problem");
+ ret = cd_profiles_old_mmc(fd);
+ }
+ goto out;
+ }
+
+ cur_profile = features[6] << 8 | features[7];
+ if (cur_profile > 0) {
+ log_debug("current profile 0x%02x", cur_profile);
+ feature_profile_media(cur_profile);
+ ret = 0; /* we have media */
+ } else
+ log_debug("no current profile, assuming no media");
+
+ len = features[0] << 24 | features[1] << 16 | features[2] << 8 | features[3];
+ log_debug("GET CONFIGURATION: size of features buffer 0x%04x", 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, 0x46);
+ scsi_cmd_set(&sc, 7, ( len >> 8 ) & 0xff);
+ scsi_cmd_set(&sc, 8, len & 0xff);
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, features, len);
+ if (err != 0) {
+ info_scsi_cmd_err("GET CONFIGURATION", err);
+ return -1;
+ }
+
+ /* parse the length once more, in case the drive decided to have other features suddenly :) */
+ len = features[0] << 24 | features[1] << 16 | features[2] << 8 | features[3];
+ log_debug("GET CONFIGURATION: size of features buffer 0x%04x", len);
+
+ if (len > sizeof(features)) {
+ log_debug("cannot get features in a single query, truncating");
+ len = sizeof(features);
+ }
+
+ /* device features */
+ for (i = 8; i+4 < len; i += (4 + features[i+3])) {
+ unsigned feature;
+
+ feature = features[i] << 8 | features[i+1];
+
+ switch (feature) {
+ case 0x00:
+ log_debug("GET CONFIGURATION: feature 'profiles', with %i entries", features[i+3] / 4);
+ feature_profiles(&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;
+ }
+ }
+out:
+ return ret;
+}
+
+static int cd_media_info(int fd) {
+ struct scsi_cmd sc;
+ unsigned char header[32];
+ static const char *const media_status[] = {
+ "blank",
+ "appendable",
+ "complete",
+ "other"
+ };
+ int err;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x51);
+ scsi_cmd_set(&sc, 8, sizeof(header) & 0xff);
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, header, sizeof(header));
+ if (err != 0) {
+ info_scsi_cmd_err("READ DISC INFORMATION", err);
+ return -1;
+ };
+
+ cd_media = 1;
+ log_debug("disk type %02x", header[8]);
+ log_debug("hardware reported media status: %s", media_status[header[2] & 3]);
+
+ /* exclude plain CDROM, some fake cdroms return 0 for "blank" media here */
+ if (!cd_media_cd_rom)
+ cd_media_state = media_status[header[2] & 3];
+
+ /* 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 (cd_media_dvd_rw_ro && (header[2] & 3) == 1)
+ cd_media_state = media_status[0];
+
+ /* 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 ((cd_media_dvd_rw_ro || cd_media_dvd_plus_rw || cd_media_dvd_plus_rw_dl || cd_media_dvd_ram) && (header[2] & 3) > 1) {
+ unsigned char buffer[32 * 2048];
+ unsigned char len;
+ int offset;
+
+ if (cd_media_dvd_ram) {
+ /* a write protected dvd-ram may report "complete" status */
+
+ unsigned char dvdstruct[8];
+ unsigned char format[12];
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0xAD);
+ scsi_cmd_set(&sc, 7, 0xC0);
+ scsi_cmd_set(&sc, 9, sizeof(dvdstruct));
+ scsi_cmd_set(&sc, 11, 0);
+ err = scsi_cmd_run(&sc, fd, dvdstruct, sizeof(dvdstruct));
+ if (err != 0) {
+ info_scsi_cmd_err("READ DVD STRUCTURE", err);
+ return -1;
+ }
+ if (dvdstruct[4] & 0x02) {
+ cd_media_state = media_status[2];
+ log_debug("write-protected DVD-RAM media inserted");
+ goto determined;
+ }
+
+ /* let's make sure we don't try to read unformatted media */
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x23);
+ scsi_cmd_set(&sc, 8, sizeof(format));
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, format, sizeof(format));
+ if (err != 0) {
+ info_scsi_cmd_err("READ DVD FORMAT CAPACITIES", err);
+ return -1;
+ }
+
+ 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:
+ log_debug("unformatted DVD-RAM media inserted");
+ /* 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 */
+ goto determined;
+
+ case 2:
+ log_debug("formatted DVD-RAM media inserted");
+ break;
+
+ case 3:
+ cd_media = 0; //return no media
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
+ "format capacities returned no media");
+ }
+ }
+
+ /* 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, 0x28);
+ scsi_cmd_set(&sc, 5, 0);
+ scsi_cmd_set(&sc, 8, 32);
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, buffer, sizeof(buffer));
+ if (err != 0) {
+ cd_media = 0;
+ info_scsi_cmd_err("READ FIRST 32 BLOCKS", err);
+ return -1;
+ }
+
+ /* 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 (offset = 32768; offset < (32768 + 2048); offset++) {
+ if (buffer [offset]) {
+ log_debug("data in block 16, assuming complete");
+ goto determined;
+ }
+ }
+
+ for (offset = 0; offset < 2048; offset++) {
+ if (buffer [offset]) {
+ log_debug("data in block 0, assuming complete");
+ goto determined;
+ }
+ }
+
+ cd_media_state = media_status[0];
+ log_debug("no data in blocks 0 or 16, assuming blank");
+ }
+
+determined:
+ /* "other" is e. g. DVD-RAM, can't append sessions there; DVDs in
+ * restricted overwrite mode can never append, only in sequential mode */
+ if ((header[2] & 3) < 2 && !cd_media_dvd_rw_ro)
+ cd_media_session_next = header[10] << 8 | header[5];
+ cd_media_session_count = header[9] << 8 | header[4];
+ cd_media_track_count = header[11] << 8 | header[6];
+
+ return 0;
+}
+
+static int cd_media_toc(int fd) {
+ struct scsi_cmd sc;
+ unsigned char header[12];
+ unsigned char toc[65536];
+ unsigned len, i, num_tracks;
+ unsigned char *p;
+ int err;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x43);
+ scsi_cmd_set(&sc, 6, 1);
+ scsi_cmd_set(&sc, 8, sizeof(header) & 0xff);
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, header, sizeof(header));
+ if (err != 0) {
+ info_scsi_cmd_err("READ TOC", err);
+ return -1;
+ }
+
+ len = (header[0] << 8 | header[1]) + 2;
+ log_debug("READ TOC: len: %d, start track: %d, end track: %d", len, header[2], header[3]);
+ if (len > sizeof(toc))
+ return -1;
+ if (len < 2)
+ return -1;
+ /* 2: first track, 3: last track */
+ num_tracks = header[3] - header[2] + 1;
+
+ /* empty media has no tracks */
+ if (len < 8)
+ return 0;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x43);
+ 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);
+ err = scsi_cmd_run(&sc, fd, toc, len);
+ if (err != 0) {
+ info_scsi_cmd_err("READ TOC (tracks)", err);
+ return -1;
+ }
+
+ /* 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 (p = toc+4, i = 4; i < len-8 && num_tracks > 0; i += 8, p += 8, --num_tracks) {
+ unsigned block;
+ unsigned is_data_track;
+
+ is_data_track = (p[1] & 0x04) != 0;
+
+ block = p[4] << 24 | p[5] << 16 | p[6] << 8 | p[7];
+ log_debug("track=%u info=0x%x(%s) start_block=%u",
+ p[2], p[1] & 0x0f, is_data_track ? "data":"audio", block);
+
+ if (is_data_track)
+ cd_media_track_count_data++;
+ else
+ cd_media_track_count_audio++;
+ }
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, 0x43);
+ scsi_cmd_set(&sc, 2, 1); /* Session Info */
+ scsi_cmd_set(&sc, 8, sizeof(header));
+ scsi_cmd_set(&sc, 9, 0);
+ err = scsi_cmd_run(&sc, fd, header, sizeof(header));
+ if (err != 0) {
+ info_scsi_cmd_err("READ TOC (multi session)", err);
+ return -1;
+ }
+ len = header[4+4] << 24 | header[4+5] << 16 | header[4+6] << 8 | header[4+7];
+ log_debug("last track %u starts at block %u", header[4+2], len);
+ cd_media_session_last_offset = (unsigned long long int)len * 2048;
+ return 0;
+}
+
+int main(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' },
+ {}
+ };
+ bool eject = false;
+ bool lock = false;
+ bool unlock = false;
+ const char *node = NULL;
+ int fd = -1;
+ int cnt;
+ int rc = 0;
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ for (;;) {
+ int option;
+
+ option = getopt_long(argc, argv, "deluh", options, NULL);
+ if (option == -1)
+ break;
+
+ switch (option) {
+ case 'l':
+ lock = true;
+ break;
+ case 'u':
+ unlock = true;
+ break;
+ case 'e':
+ eject = true;
+ break;
+ case 'd':
+ log_set_target(LOG_TARGET_CONSOLE);
+ log_set_max_level(LOG_DEBUG);
+ log_open();
+ break;
+ case 'h':
+ printf("Usage: %s [options] <device>\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 debug to stderr\n"
+ " -h,--help print this help text\n\n",
+ program_invocation_short_name);
+ goto exit;
+ default:
+ rc = 1;
+ goto exit;
+ }
+ }
+
+ node = argv[optind];
+ if (!node) {
+ log_error("no device");
+ rc = 1;
+ goto exit;
+ }
+
+ initialize_srand();
+ for (cnt = 20; cnt > 0; cnt--) {
+ struct timespec duration;
+
+ fd = open(node, O_RDONLY|O_NONBLOCK|O_CLOEXEC);
+ if (fd >= 0 || errno != EBUSY)
+ break;
+ duration.tv_sec = 0;
+ duration.tv_nsec = (100 * 1000 * 1000) + (rand() % 100 * 1000 * 1000);
+ nanosleep(&duration, NULL);
+ }
+ if (fd < 0) {
+ log_debug("unable to open '%s'", node);
+ rc = 1;
+ goto exit;
+ }
+ log_debug("probing: '%s'", node);
+
+ /* same data as original cdrom_id */
+ if (cd_capability_compat(fd) < 0) {
+ rc = 1;
+ goto exit;
+ }
+
+ /* check for media - don't bail if there's no media as we still need to
+ * to read profiles */
+ cd_media_compat(fd);
+
+ /* check if drive talks MMC */
+ if (cd_inquiry(fd) < 0)
+ goto work;
+
+ /* read drive and possibly current profile */
+ if (cd_profiles(fd) != 0)
+ goto work;
+
+ /* at this point we are guaranteed to have media in the drive - find out more about it */
+
+ /* get session/track info */
+ cd_media_toc(fd);
+
+ /* get writable media state */
+ cd_media_info(fd);
+
+work:
+ /* lock the media, so we enable eject button events */
+ if (lock && cd_media) {
+ log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (lock)");
+ media_lock(fd, true);
+ }
+
+ if (unlock && cd_media) {
+ log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)");
+ media_lock(fd, false);
+ }
+
+ if (eject) {
+ log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)");
+ media_lock(fd, false);
+ log_debug("START_STOP_UNIT (eject)");
+ media_eject(fd);
+ }
+
+ printf("ID_CDROM=1\n");
+ if (cd_cd_rom)
+ printf("ID_CDROM_CD=1\n");
+ if (cd_cd_r)
+ printf("ID_CDROM_CD_R=1\n");
+ if (cd_cd_rw)
+ printf("ID_CDROM_CD_RW=1\n");
+ if (cd_dvd_rom)
+ printf("ID_CDROM_DVD=1\n");
+ if (cd_dvd_r)
+ printf("ID_CDROM_DVD_R=1\n");
+ if (cd_dvd_rw)
+ printf("ID_CDROM_DVD_RW=1\n");
+ if (cd_dvd_ram)
+ printf("ID_CDROM_DVD_RAM=1\n");
+ if (cd_dvd_plus_r)
+ printf("ID_CDROM_DVD_PLUS_R=1\n");
+ if (cd_dvd_plus_rw)
+ printf("ID_CDROM_DVD_PLUS_RW=1\n");
+ if (cd_dvd_plus_r_dl)
+ printf("ID_CDROM_DVD_PLUS_R_DL=1\n");
+ if (cd_dvd_plus_rw_dl)
+ printf("ID_CDROM_DVD_PLUS_RW_DL=1\n");
+ if (cd_bd)
+ printf("ID_CDROM_BD=1\n");
+ if (cd_bd_r)
+ printf("ID_CDROM_BD_R=1\n");
+ if (cd_bd_re)
+ printf("ID_CDROM_BD_RE=1\n");
+ if (cd_hddvd)
+ printf("ID_CDROM_HDDVD=1\n");
+ if (cd_hddvd_r)
+ printf("ID_CDROM_HDDVD_R=1\n");
+ if (cd_hddvd_rw)
+ printf("ID_CDROM_HDDVD_RW=1\n");
+ if (cd_mo)
+ printf("ID_CDROM_MO=1\n");
+ if (cd_mrw)
+ printf("ID_CDROM_MRW=1\n");
+ if (cd_mrw_w)
+ printf("ID_CDROM_MRW_W=1\n");
+
+ if (cd_media)
+ printf("ID_CDROM_MEDIA=1\n");
+ if (cd_media_mo)
+ printf("ID_CDROM_MEDIA_MO=1\n");
+ if (cd_media_mrw)
+ printf("ID_CDROM_MEDIA_MRW=1\n");
+ if (cd_media_mrw_w)
+ printf("ID_CDROM_MEDIA_MRW_W=1\n");
+ if (cd_media_cd_rom)
+ printf("ID_CDROM_MEDIA_CD=1\n");
+ if (cd_media_cd_r)
+ printf("ID_CDROM_MEDIA_CD_R=1\n");
+ if (cd_media_cd_rw)
+ printf("ID_CDROM_MEDIA_CD_RW=1\n");
+ if (cd_media_dvd_rom)
+ printf("ID_CDROM_MEDIA_DVD=1\n");
+ if (cd_media_dvd_r)
+ printf("ID_CDROM_MEDIA_DVD_R=1\n");
+ if (cd_media_dvd_ram)
+ printf("ID_CDROM_MEDIA_DVD_RAM=1\n");
+ if (cd_media_dvd_rw)
+ printf("ID_CDROM_MEDIA_DVD_RW=1\n");
+ if (cd_media_dvd_plus_r)
+ printf("ID_CDROM_MEDIA_DVD_PLUS_R=1\n");
+ if (cd_media_dvd_plus_rw)
+ printf("ID_CDROM_MEDIA_DVD_PLUS_RW=1\n");
+ if (cd_media_dvd_plus_rw_dl)
+ printf("ID_CDROM_MEDIA_DVD_PLUS_RW_DL=1\n");
+ if (cd_media_dvd_plus_r_dl)
+ printf("ID_CDROM_MEDIA_DVD_PLUS_R_DL=1\n");
+ if (cd_media_bd)
+ printf("ID_CDROM_MEDIA_BD=1\n");
+ if (cd_media_bd_r)
+ printf("ID_CDROM_MEDIA_BD_R=1\n");
+ if (cd_media_bd_re)
+ printf("ID_CDROM_MEDIA_BD_RE=1\n");
+ if (cd_media_hddvd)
+ printf("ID_CDROM_MEDIA_HDDVD=1\n");
+ if (cd_media_hddvd_r)
+ printf("ID_CDROM_MEDIA_HDDVD_R=1\n");
+ if (cd_media_hddvd_rw)
+ printf("ID_CDROM_MEDIA_HDDVD_RW=1\n");
+
+ if (cd_media_state)
+ printf("ID_CDROM_MEDIA_STATE=%s\n", cd_media_state);
+ if (cd_media_session_next > 0)
+ printf("ID_CDROM_MEDIA_SESSION_NEXT=%u\n", cd_media_session_next);
+ if (cd_media_session_count > 0)
+ printf("ID_CDROM_MEDIA_SESSION_COUNT=%u\n", cd_media_session_count);
+ if (cd_media_session_count > 1 && cd_media_session_last_offset > 0)
+ printf("ID_CDROM_MEDIA_SESSION_LAST_OFFSET=%llu\n", cd_media_session_last_offset);
+ if (cd_media_track_count > 0)
+ printf("ID_CDROM_MEDIA_TRACK_COUNT=%u\n", cd_media_track_count);
+ if (cd_media_track_count_audio > 0)
+ printf("ID_CDROM_MEDIA_TRACK_COUNT_AUDIO=%u\n", cd_media_track_count_audio);
+ if (cd_media_track_count_data > 0)
+ printf("ID_CDROM_MEDIA_TRACK_COUNT_DATA=%u\n", cd_media_track_count_data);
+exit:
+ if (fd >= 0)
+ close(fd);
+ log_close();
+ return rc;
+}
diff --git a/src/udev/fido_id/fido_id.c b/src/udev/fido_id/fido_id.c
new file mode 100644
index 0000000..f14b81d
--- /dev/null
+++ b/src/udev/fido_id/fido_id.c
@@ -0,0 +1,96 @@
+/* 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 <linux/hid.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "device-internal.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 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 = -1;
+
+ 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();
+
+ if (argc > 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s [SYSFS_PATH]", program_invocation_short_name);
+
+ if (argc == 1) {
+ r = device_new_from_strv(&device, environ);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get current device from environment: %m");
+ } else {
+ r = sd_device_new_from_syspath(&device, argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get device from syspath: %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);
+ 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..44d66df
--- /dev/null
+++ b/src/udev/fido_id/fuzz-fido-id-desc.c
@@ -0,0 +1,23 @@
+/* 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) {
+ /* We don't want to fill the logs with messages about parse errors.
+ * Disable most logging if not running standalone */
+ if (!getenv("SYSTEMD_LOG_LEVEL"))
+ log_set_max_level(LOG_CRIT);
+
+ if (size > 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..6836bca
--- /dev/null
+++ b/src/udev/fido_id/test-fido-id-desc.c
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "fido_id_desc.h"
+#include "macro.h"
+
+static void test_is_fido_security_token_desc__fido(void) {
+ 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);
+}
+
+static void test_is_fido_security_token_desc__non_fido(void) {
+ /* 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);
+}
+
+static void test_is_fido_security_token_desc__invalid(void) {
+ /* 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);
+}
+
+int main(int argc, char *argv[]) {
+ test_is_fido_security_token_desc__fido();
+ test_is_fido_security_token_desc__non_fido();
+ test_is_fido_security_token_desc__invalid();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/udev/generate-keyboard-keys-gperf.sh b/src/udev/generate-keyboard-keys-gperf.sh
new file mode 100755
index 0000000..c78652a
--- /dev/null
+++ b/src/udev/generate-keyboard-keys-gperf.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -eu
+
+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..aa00c15
--- /dev/null
+++ b/src/udev/generate-keyboard-keys-list.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+
+$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/meson.build b/src/udev/meson.build
new file mode 100644
index 0000000..5eb0f99
--- /dev/null
+++ b/src/udev/meson.build
@@ -0,0 +1,225 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+udevadm_sources = files('''
+ udevadm.c
+ udevadm.h
+ udevadm-control.c
+ udevadm-hwdb.c
+ udevadm-info.c
+ udevadm-monitor.c
+ udevadm-settle.c
+ udevadm-test.c
+ udevadm-test-builtin.c
+ udevadm-trigger.c
+ udevadm-util.c
+ udevadm-util.h
+ udevd.c
+'''.split())
+
+libudev_core_sources = '''
+ udev-ctrl.c
+ udev-ctrl.h
+ udev-event.c
+ udev-event.h
+ udev-node.c
+ udev-node.h
+ udev-rules.c
+ udev-rules.h
+ udev-watch.c
+ udev-watch.h
+ udev-builtin.c
+ udev-builtin.h
+ udev-builtin-btrfs.c
+ udev-builtin-hwdb.c
+ udev-builtin-input_id.c
+ udev-builtin-keyboard.c
+ udev-builtin-net_id.c
+ udev-builtin-net_setup_link.c
+ udev-builtin-path_id.c
+ udev-builtin-usb_id.c
+ net/link-config.c
+ net/link-config.h
+'''.split()
+
+if conf.get('HAVE_KMOD') == 1
+ libudev_core_sources += ['udev-builtin-kmod.c']
+endif
+
+if conf.get('HAVE_BLKID') == 1
+ libudev_core_sources += ['udev-builtin-blkid.c']
+endif
+
+if conf.get('HAVE_ACL') == 1
+ libudev_core_sources += ['udev-builtin-uaccess.c',
+ logind_acl_c,
+ sd_login_sources]
+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)
+
+############################################################
+
+link_config_gperf_c = custom_target(
+ 'link-config-gperf.c',
+ input : 'net/link-config-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 = rootlibexecdir
+else
+ udev_link_with = [libshared_static,
+ libsystemd_static]
+ udev_rpath = ''
+endif
+
+libudev_basic = static_library(
+ 'udev-basic',
+ libudev_sources,
+ include_directories : includes,
+ c_args : ['-fvisibility=default'])
+
+libudev_static = static_library(
+ 'udev_static',
+ include_directories : includes,
+ link_with : udev_link_with,
+ link_whole : libudev_basic)
+
+static_libudev = get_option('static-libudev')
+static_libudev_pic = static_libudev == 'true' or static_libudev == 'pic'
+install_libudev_static = static_library(
+ 'udev',
+ basic_sources,
+ shared_sources,
+ libsystemd_sources,
+ libudev_sources,
+ disable_mempool_c,
+ include_directories : includes,
+ build_by_default : static_libudev != 'false',
+ install : static_libudev != 'false',
+ install_dir : rootlibdir,
+ link_depends : libudev_sym,
+ dependencies : libshared_deps + [libmount],
+ c_args : static_libudev_pic ? [] : ['-fno-PIC'],
+ pic : static_libudev_pic)
+
+libudev = shared_library(
+ 'udev',
+ disable_mempool_c,
+ version : libudev_version,
+ include_directories : includes,
+ link_args : ['-shared',
+ '-Wl,--version-script=' + libudev_sym_path],
+ link_with : [libsystemd_static, libshared_static],
+ link_whole : libudev_basic,
+ dependencies : [threads],
+ link_depends : libudev_sym,
+ install : true,
+ install_dir : rootlibdir)
+
+libudev_core_includes = [includes, include_directories('net')]
+libudev_core = static_library(
+ 'udev-core',
+ libudev_core_sources,
+ link_config_gperf_c,
+ keyboard_keys_from_name_h,
+ include_directories : libudev_core_includes,
+ c_args : ['-DLOG_REALM=LOG_REALM_UDEV'],
+ link_with : udev_link_with,
+ dependencies : [libblkid, libkmod])
+
+foreach prog : [['ata_id/ata_id.c'],
+ ['cdrom_id/cdrom_id.c'],
+ ['fido_id/fido_id.c',
+ 'fido_id/fido_id_desc.c',
+ 'fido_id/fido_id_desc.h'],
+ ['scsi_id/scsi_id.c',
+ 'scsi_id/scsi_id.h',
+ 'scsi_id/scsi_serial.c',
+ 'scsi_id/scsi.h'],
+ ['v4l_id/v4l_id.c'],
+ ['mtd_probe/mtd_probe.c',
+ 'mtd_probe/mtd_probe.h',
+ 'mtd_probe/probe_smartmedia.c']]
+
+ executable(prog[0].split('/')[0],
+ prog,
+ include_directories : includes,
+ c_args : ['-DLOG_REALM=LOG_REALM_UDEV'],
+ dependencies : [versiondep],
+ link_with : [libudev_static],
+ install_rpath : udev_rpath,
+ install : true,
+ install_dir : udevlibexecdir)
+endforeach
+
+if install_sysconfdir
+ install_data('udev.conf',
+ install_dir : join_paths(sysconfdir, 'udev'))
+endif
+
+configure_file(
+ input : 'udev.pc.in',
+ output : 'udev.pc',
+ configuration : substs,
+ install_dir : pkgconfigdatadir == 'no' ? '' : pkgconfigdatadir)
+
+if install_sysconfdir
+ meson.add_install_script('sh', '-c',
+ mkdir_p.format(join_paths(sysconfdir, 'udev/rules.d')))
+endif
+
+fuzzers += [
+ [['src/udev/net/fuzz-link-parser.c',
+ 'src/fuzz/fuzz.h'],
+ [libudev_core,
+ libudev_static,
+ libsystemd_network,
+ libshared],
+ [threads,
+ libacl]],
+
+ [['src/udev/fido_id/fuzz-fido-id-desc.c',
+ 'src/udev/fido_id/fido_id_desc.c'],
+ [],
+ []],
+ ]
+
+tests += [
+ [['src/udev/fido_id/test-fido-id-desc.c',
+ 'src/udev/fido_id/fido_id_desc.c'],
+ [],
+ []],
+ ]
diff --git a/src/udev/mtd_probe/mtd_probe.c b/src/udev/mtd_probe/mtd_probe.c
new file mode 100644
index 0000000..df1f1c1
--- /dev/null
+++ b/src/udev/mtd_probe/mtd_probe.c
@@ -0,0 +1,59 @@
+/* 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 <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 "fd-util.h"
+#include "mtd_probe.h"
+
+int main(int argc, char** argv) {
+ _cleanup_close_ int mtd_fd = -1;
+ mtd_info_t mtd_info;
+
+ if (argc != 2) {
+ printf("usage: mtd_probe /dev/mtd[n]\n");
+ return EXIT_FAILURE;
+ }
+
+ mtd_fd = open(argv[1], O_RDONLY|O_CLOEXEC);
+ if (mtd_fd < 0) {
+ log_error_errno(errno, "Failed to open: %m");
+ return EXIT_FAILURE;
+ }
+
+ if (ioctl(mtd_fd, MEMGETINFO, &mtd_info) < 0) {
+ log_error_errno(errno, "Failed to issue MEMGETINFO ioctl: %m");
+ return EXIT_FAILURE;
+ }
+
+ if (probe_smart_media(mtd_fd, &mtd_info) < 0)
+ return EXIT_FAILURE;
+
+ return EXIT_SUCCESS;
+}
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..f4612ba
--- /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..6f3469c
--- /dev/null
+++ b/src/udev/net/fuzz-link-parser.c
@@ -0,0 +1,28 @@
+/* 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) link_config_ctx *ctx = NULL;
+ _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-link-config.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ if (size > 65535)
+ return 0;
+
+ if (!getenv("SYSTEMD_LOG_LEVEL"))
+ log_set_max_level(LOG_CRIT);
+
+ 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..0824b19
--- /dev/null
+++ b/src/udev/net/fuzz-link-parser.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65535
diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf
new file mode 100644
index 0000000..20f5d7e
--- /dev/null
+++ b/src/udev/net/link-config-gperf.gperf
@@ -0,0 +1,68 @@
+%{
+#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 "network-internal.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_hwaddrs, 0, offsetof(link_config, match_mac)
+Match.PermanentMACAddress, config_parse_hwaddrs, 0, offsetof(link_config, match_permanent_mac)
+Match.OriginalName, config_parse_match_ifnames, 0, offsetof(link_config, match_name)
+Match.Path, config_parse_match_strv, 0, offsetof(link_config, match_path)
+Match.Driver, config_parse_match_strv, 0, offsetof(link_config, match_driver)
+Match.Type, config_parse_match_strv, 0, offsetof(link_config, match_type)
+Match.Property, config_parse_match_property, 0, offsetof(link_config, match_property)
+Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(link_config, conditions)
+Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(link_config, conditions)
+Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(link_config, conditions)
+Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(link_config, conditions)
+Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(link_config, conditions)
+Link.Description, config_parse_string, 0, offsetof(link_config, description)
+Link.MACAddressPolicy, config_parse_mac_address_policy, 0, offsetof(link_config, mac_address_policy)
+Link.MACAddress, config_parse_hwaddr, 0, offsetof(link_config, mac)
+Link.NamePolicy, config_parse_name_policy, 0, offsetof(link_config, name_policy)
+Link.Name, config_parse_ifname, 0, offsetof(link_config, name)
+Link.AlternativeName, config_parse_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(link_config, alternative_names)
+Link.AlternativeNamesPolicy, config_parse_alternative_names_policy, 0, offsetof(link_config, alternative_names_policy)
+Link.Alias, config_parse_ifalias, 0, offsetof(link_config, alias)
+Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(link_config, mtu)
+Link.BitsPerSecond, config_parse_si_uint64, 0, offsetof(link_config, speed)
+Link.Duplex, config_parse_duplex, 0, offsetof(link_config, duplex)
+Link.AutoNegotiation, config_parse_tristate, 0, offsetof(link_config, autonegotiation)
+Link.WakeOnLan, config_parse_wol, 0, offsetof(link_config, wol)
+Link.Port, config_parse_port, 0, offsetof(link_config, port)
+Link.ReceiveChecksumOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_RX])
+Link.TransmitChecksumOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_TX])
+Link.GenericSegmentationOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_GSO])
+Link.TCPSegmentationOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_TSO])
+Link.TCP6SegmentationOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_TSO6])
+Link.UDPSegmentationOffload, config_parse_warn_compat, DISABLED_LEGACY, 0
+Link.GenericReceiveOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_GRO])
+Link.LargeReceiveOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_LRO])
+Link.RxChannels, config_parse_channel, 0, offsetof(link_config, channels)
+Link.TxChannels, config_parse_channel, 0, offsetof(link_config, channels)
+Link.OtherChannels, config_parse_channel, 0, offsetof(link_config, channels)
+Link.CombinedChannels, config_parse_channel, 0, offsetof(link_config, channels)
+Link.Advertise, config_parse_advertise, 0, offsetof(link_config, advertise)
+Link.RxBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring)
+Link.RxMiniBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring)
+Link.RxJumboBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring)
+Link.TxBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring)
+Link.RxFlowControl, config_parse_tristate, 0, offsetof(link_config, rx_flow_control)
+Link.TxFlowControl, config_parse_tristate, 0, offsetof(link_config, tx_flow_control)
+Link.AutoNegotiationFlowControl, config_parse_tristate, 0, offsetof(link_config, autoneg_flow_control)
diff --git a/src/udev/net/link-config.c b/src/udev/net/link-config.c
new file mode 100644
index 0000000..d12fd0e
--- /dev/null
+++ b/src/udev/net/link-config.c
@@ -0,0 +1,711 @@
+/* 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 "conf-files.h"
+#include "conf-parser.h"
+#include "def.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "ethtool-util.h"
+#include "fd-util.h"
+#include "link-config.h"
+#include "log.h"
+#include "memory-util.h"
+#include "netif-naming-scheme.h"
+#include "netlink-util.h"
+#include "network-internal.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"
+
+struct link_config_ctx {
+ LIST_HEAD(link_config, links);
+
+ int ethtool_fd;
+
+ bool enable_name_policy;
+
+ sd_netlink *rtnl;
+
+ usec_t network_dirs_ts_usec;
+};
+
+static void link_config_free(link_config *link) {
+ if (!link)
+ return;
+
+ free(link->filename);
+
+ set_free_free(link->match_mac);
+ set_free_free(link->match_permanent_mac);
+ strv_free(link->match_path);
+ strv_free(link->match_driver);
+ strv_free(link->match_type);
+ strv_free(link->match_name);
+ strv_free(link->match_property);
+ condition_free_list(link->conditions);
+
+ free(link->description);
+ free(link->mac);
+ free(link->name_policy);
+ free(link->name);
+ strv_free(link->alternative_names);
+ free(link->alternative_names_policy);
+ free(link->alias);
+
+ free(link);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(link_config*, link_config_free);
+
+static void link_configs_free(link_config_ctx *ctx) {
+ link_config *link, *link_next;
+
+ if (!ctx)
+ return;
+
+ LIST_FOREACH_SAFE(links, link, link_next, ctx->links)
+ link_config_free(link);
+}
+
+void link_config_ctx_free(link_config_ctx *ctx) {
+ if (!ctx)
+ return;
+
+ safe_close(ctx->ethtool_fd);
+
+ sd_netlink_unref(ctx->rtnl);
+
+ link_configs_free(ctx);
+
+ free(ctx);
+
+ return;
+}
+
+int link_config_ctx_new(link_config_ctx **ret) {
+ _cleanup_(link_config_ctx_freep) link_config_ctx *ctx = NULL;
+
+ if (!ret)
+ return -EINVAL;
+
+ ctx = new0(link_config_ctx, 1);
+ if (!ctx)
+ return -ENOMEM;
+
+ LIST_HEAD_INIT(ctx->links);
+
+ ctx->ethtool_fd = -1;
+
+ ctx->enable_name_policy = true;
+
+ *ret = TAKE_PTR(ctx);
+
+ return 0;
+}
+
+int link_load_one(link_config_ctx *ctx, const char *filename) {
+ _cleanup_(link_config_freep) link_config *link = NULL;
+ _cleanup_fclose_ FILE *file = NULL;
+ _cleanup_free_ char *name = NULL;
+ size_t i;
+ int r;
+
+ assert(ctx);
+ assert(filename);
+
+ file = fopen(filename, "re");
+ if (!file)
+ return errno == ENOENT ? 0 : -errno;
+
+ if (null_or_empty_fd(fileno(file))) {
+ log_debug("Skipping empty file: %s", filename);
+ return 0;
+ }
+
+ name = strdup(filename);
+ if (!name)
+ return -ENOMEM;
+
+ link = new(link_config, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (link_config) {
+ .filename = TAKE_PTR(name),
+ .mac_address_policy = _MAC_ADDRESS_POLICY_INVALID,
+ .wol = _WOL_INVALID,
+ .duplex = _DUP_INVALID,
+ .port = _NET_DEV_PORT_INVALID,
+ .autonegotiation = -1,
+ .rx_flow_control = -1,
+ .tx_flow_control = -1,
+ .autoneg_flow_control = -1,
+ };
+
+ for (i = 0; i < ELEMENTSOF(link->features); i++)
+ link->features[i] = -1;
+
+ r = config_parse(NULL, filename, file,
+ "Match\0Link\0",
+ config_item_perf_lookup, link_config_gperf_lookup,
+ CONFIG_PARSE_WARN, link,
+ NULL);
+ if (r < 0)
+ return r;
+
+ if (set_isempty(link->match_mac) && set_isempty(link->match_permanent_mac) &&
+ strv_isempty(link->match_path) && strv_isempty(link->match_driver) && strv_isempty(link->match_type) &&
+ strv_isempty(link->match_name) && strv_isempty(link->match_property) && !link->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(link->conditions, environ, NULL, NULL, NULL)) {
+ log_debug("%s: Conditions do not match the system environment, skipping.", filename);
+ return 0;
+ }
+
+ if (IN_SET(link->mac_address_policy, MAC_ADDRESS_POLICY_PERSISTENT, MAC_ADDRESS_POLICY_RANDOM) && link->mac) {
+ log_warning("%s: MACAddress= in [Link] section will be ignored when MACAddressPolicy= "
+ "is set to \"persistent\" or \"random\".",
+ filename);
+ link->mac = mfree(link->mac);
+ }
+
+ log_debug("Parsed configuration file %s", filename);
+
+ LIST_PREPEND(links, ctx->links, TAKE_PTR(link));
+ return 0;
+}
+
+static bool enable_name_policy(void) {
+ bool b;
+
+ return proc_cmdline_get_bool("net.ifnames", &b) <= 0 || b;
+}
+
+static int link_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(link_config_ctx *ctx) {
+ _cleanup_strv_free_ char **files = NULL;
+ char **f;
+ int r;
+
+ link_configs_free(ctx);
+
+ if (!enable_name_policy()) {
+ ctx->enable_name_policy = false;
+ log_info("Network interface NamePolicy= disabled on kernel command line, ignoring.");
+ }
+
+ /* update timestamp */
+ paths_check_timestamp(NETWORK_DIRS, &ctx->network_dirs_ts_usec, true);
+
+ 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) {
+ r = link_load_one(ctx, *f);
+ if (r < 0)
+ log_error_errno(r, "Failed to load %s, ignoring: %m", *f);
+ }
+
+ return 0;
+}
+
+bool link_config_should_reload(link_config_ctx *ctx) {
+ return paths_check_timestamp(NETWORK_DIRS, &ctx->network_dirs_ts_usec, false);
+}
+
+int link_config_get(link_config_ctx *ctx, sd_device *device, link_config **ret) {
+ unsigned name_assign_type = NET_NAME_UNKNOWN;
+ struct ether_addr permanent_mac = {};
+ unsigned short iftype = 0;
+ link_config *link;
+ const char *name;
+ int ifindex, r;
+
+ assert(ctx);
+ assert(device);
+ assert(ret);
+
+ r = sd_device_get_sysname(device, &name);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_ifindex(device, &ifindex);
+ if (r < 0)
+ return r;
+
+ r = rtnl_get_link_iftype(&ctx->rtnl, ifindex, &iftype);
+ if (r < 0)
+ return r;
+
+ r = ethtool_get_permanent_macaddr(&ctx->ethtool_fd, name, &permanent_mac);
+ if (r < 0)
+ log_device_debug_errno(device, r, "Failed to get permanent MAC address, ignoring: %m");
+
+ (void) link_unsigned_attribute(device, "name_assign_type", &name_assign_type);
+
+ LIST_FOREACH(links, link, ctx->links) {
+ if (net_match_config(link->match_mac, link->match_permanent_mac, link->match_path, link->match_driver,
+ link->match_type, link->match_name, link->match_property, NULL, NULL, NULL,
+ device, NULL, &permanent_mac, NULL, iftype, NULL, NULL, 0, NULL, NULL)) {
+
+ if (link->match_name && !strv_contains(link->match_name, "*") && name_assign_type == NET_NAME_ENUM)
+ log_device_warning(device, "Config file %s is applied to device based on potentially unpredictable interface name.",
+ link->filename);
+ else
+ log_device_debug(device, "Config file %s is applied", link->filename);
+
+ *ret = link;
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+static int link_config_apply_ethtool_settings(int *ethtool_fd, const link_config *config, sd_device *device) {
+ const char *name;
+ int r;
+
+ assert(ethtool_fd);
+ assert(config);
+ assert(device);
+
+ r = sd_device_get_sysname(device, &name);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get sysname: %m");
+
+ r = ethtool_set_glinksettings(ethtool_fd, name,
+ config->autonegotiation, config->advertise,
+ config->speed, config->duplex, config->port);
+ if (r < 0) {
+ if (config->port != _NET_DEV_PORT_INVALID)
+ log_device_warning_errno(device, r, "Could not set port '%s', ignoring: %m", port_to_string(config->port));
+
+ if (!eqzero(config->advertise))
+ log_device_warning_errno(device, r, "Could not set advertise mode, ignoring: %m"); /* TODO: include modes in the log message. */
+
+ if (config->speed) {
+ unsigned speed = DIV_ROUND_UP(config->speed, 1000000);
+ if (r == -EOPNOTSUPP) {
+ r = ethtool_set_speed(ethtool_fd, name, speed, config->duplex);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set speed to %uMbps, ignoring: %m", speed);
+ }
+ }
+
+ if (config->duplex != _DUP_INVALID)
+ log_device_warning_errno(device, r, "Could not set duplex to %s, ignoring: %m", duplex_to_string(config->duplex));
+ }
+
+ r = ethtool_set_wol(ethtool_fd, name, config->wol);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set WakeOnLan to %s, ignoring: %m", wol_to_string(config->wol));
+
+ r = ethtool_set_features(ethtool_fd, name, config->features);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set offload features, ignoring: %m");
+
+ if (config->channels.rx_count_set || config->channels.tx_count_set || config->channels.other_count_set || config->channels.combined_count_set) {
+ r = ethtool_set_channels(ethtool_fd, name, &config->channels);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set channels, ignoring: %m");
+ }
+
+ if (config->ring.rx_pending_set || config->ring.rx_mini_pending_set || config->ring.rx_jumbo_pending_set || config->ring.tx_pending_set) {
+ r = ethtool_set_nic_buffer_size(ethtool_fd, name, &config->ring);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set ring buffer, ignoring: %m");
+ }
+
+ if (config->rx_flow_control >= 0 || config->tx_flow_control >= 0 || config->autoneg_flow_control >= 0) {
+ r = ethtool_set_flow_control(ethtool_fd, name, config->rx_flow_control, config->tx_flow_control, config->autoneg_flow_control);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set flow control, ignoring: %m");
+ }
+
+ return 0;
+}
+
+static int get_mac(sd_device *device, MACAddressPolicy policy, struct ether_addr *mac) {
+ unsigned addr_type;
+ bool want_random = policy == MAC_ADDRESS_POLICY_RANDOM;
+ int r;
+
+ assert(IN_SET(policy, MAC_ADDRESS_POLICY_RANDOM, MAC_ADDRESS_POLICY_PERSISTENT));
+
+ r = link_unsigned_attribute(device, "addr_assign_type", &addr_type);
+ if (r < 0)
+ return r;
+ switch (addr_type) {
+ case NET_ADDR_SET:
+ return log_device_debug(device, "MAC on the device already set by userspace");
+ case NET_ADDR_STOLEN:
+ return log_device_debug(device, "MAC on the device already set based on another device");
+ case NET_ADDR_RANDOM:
+ case NET_ADDR_PERM:
+ break;
+ default:
+ log_device_warning(device, "Unknown addr_assign_type %u, ignoring", addr_type);
+ return 0;
+ }
+
+ if (want_random == (addr_type == NET_ADDR_RANDOM))
+ return log_device_debug(device, "MAC on the device already matches policy *%s*",
+ mac_address_policy_to_string(policy));
+
+ if (want_random) {
+ log_device_debug(device, "Using random bytes to generate MAC");
+
+ /* 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. We do allow RDRAND however, since this is not
+ * cryptographic key material. */
+ r = genuine_random_bytes(mac->ether_addr_octet, ETH_ALEN, RANDOM_ALLOW_RDRAND);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to acquire random data to generate MAC: %m");
+ } else {
+ uint64_t result;
+
+ r = net_get_unique_predictable_data(device,
+ naming_scheme_has(NAMING_STABLE_VIRTUAL_MACS),
+ &result);
+ if (r < 0)
+ return log_device_warning_errno(device, r, "Could not generate persistent MAC: %m");
+
+ log_device_debug(device, "Using generated persistent MAC address");
+ assert_cc(ETH_ALEN <= sizeof(result));
+ memcpy(mac->ether_addr_octet, &result, ETH_ALEN);
+ }
+
+ /* see eth_random_addr in the kernel */
+ mac->ether_addr_octet[0] &= 0xfe; /* clear multicast bit */
+ mac->ether_addr_octet[0] |= 0x02; /* set local assignment bit (IEEE802) */
+ return 1;
+}
+
+static int link_config_apply_rtnl_settings(sd_netlink **rtnl, const link_config *config, sd_device *device) {
+ struct ether_addr generated_mac, *mac = NULL;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(config);
+ assert(device);
+
+ r = sd_device_get_ifindex(device, &ifindex);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Could not find ifindex: %m");
+
+ if (IN_SET(config->mac_address_policy, MAC_ADDRESS_POLICY_PERSISTENT, MAC_ADDRESS_POLICY_RANDOM)) {
+ if (get_mac(device, config->mac_address_policy, &generated_mac) > 0)
+ mac = &generated_mac;
+ } else
+ mac = config->mac;
+
+ r = rtnl_set_link_properties(rtnl, ifindex, config->alias, mac, config->mtu);
+ if (r < 0)
+ log_device_warning_errno(device, r, "Could not set Alias=, MACAddress= or MTU=, ignoring: %m");
+
+ return 0;
+}
+
+static int link_config_generate_new_name(const link_config_ctx *ctx, const link_config *config, sd_device *device, const char **ret_name) {
+ unsigned name_type = NET_NAME_UNKNOWN;
+ const char *new_name = NULL;
+ NamePolicy policy;
+ int r;
+
+ assert(ctx);
+ assert(config);
+ assert(device);
+ assert(ret_name);
+
+ (void) link_unsigned_attribute(device, "name_assign_type", &name_type);
+
+ if (IN_SET(name_type, NET_NAME_USER, NET_NAME_RENAMED)
+ && !naming_scheme_has(NAMING_ALLOW_RERENAMES)) {
+ log_device_debug(device, "Device already has a name given by userspace, not renaming.");
+ goto no_rename;
+ }
+
+ if (ctx->enable_name_policy && config->name_policy)
+ for (NamePolicy *p = config->name_policy; *p != _NAMEPOLICY_INVALID; p++) {
+ policy = *p;
+
+ switch (policy) {
+ case NAMEPOLICY_KERNEL:
+ if (name_type != NET_NAME_PREDICTABLE)
+ continue;
+
+ /* The kernel claims to have given a predictable name, keep it. */
+ log_device_debug(device, "Policy *%s*: keeping predictable kernel name",
+ name_policy_to_string(policy));
+ goto no_rename;
+ case NAMEPOLICY_KEEP:
+ if (!IN_SET(name_type, NET_NAME_USER, NET_NAME_RENAMED))
+ continue;
+
+ log_device_debug(device, "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("invalid policy");
+ }
+ if (ifname_valid(new_name))
+ break;
+ }
+
+ if (new_name) {
+ log_device_debug(device, "Policy *%s* yields \"%s\".", name_policy_to_string(policy), new_name);
+ *ret_name = new_name;
+ return 0;
+ }
+
+ if (config->name) {
+ log_device_debug(device, "Policies didn't yield a name, using specified Name=%s.", config->name);
+ *ret_name = config->name;
+ return 0;
+ }
+
+ log_device_debug(device, "Policies didn't yield a name and Name= is not given, not renaming.");
+no_rename:
+ r = sd_device_get_sysname(device, ret_name);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get sysname: %m");
+
+ return 0;
+}
+
+static int link_config_apply_alternative_names(sd_netlink **rtnl, const link_config *config, sd_device *device, const char *new_name) {
+ _cleanup_strv_free_ char **altnames = NULL, **current_altnames = NULL;
+ const char *current_name;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(config);
+ assert(device);
+
+ r = sd_device_get_sysname(device, &current_name);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get sysname: %m");
+
+ r = sd_device_get_ifindex(device, &ifindex);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Could not find ifindex: %m");
+
+ 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("invalid policy");
+ }
+ if (!isempty(n)) {
+ r = strv_extend(&altnames, n);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ if (new_name)
+ strv_remove(altnames, new_name);
+ strv_remove(altnames, current_name);
+
+ r = rtnl_get_link_alternative_names(rtnl, ifindex, &current_altnames);
+ if (r < 0)
+ log_device_debug_errno(device, r, "Failed to get alternative names, ignoring: %m");
+
+ char **p;
+ STRV_FOREACH(p, current_altnames)
+ strv_remove(altnames, *p);
+
+ strv_uniq(altnames);
+ strv_sort(altnames);
+ r = rtnl_set_link_alternative_names(rtnl, ifindex, altnames);
+ if (r < 0)
+ log_device_full_errno(device, r == -EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, r,
+ "Could not set AlternativeName= or apply AlternativeNamesPolicy=, ignoring: %m");
+
+ return 0;
+}
+
+int link_config_apply(link_config_ctx *ctx, const link_config *config, sd_device *device, const char **ret_name) {
+ const char *new_name;
+ DeviceAction a;
+ int r;
+
+ assert(ctx);
+ assert(config);
+ assert(device);
+ assert(ret_name);
+
+ r = device_get_action(device, &a);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get ACTION= property: %m");
+
+ if (!IN_SET(a, DEVICE_ACTION_ADD, DEVICE_ACTION_BIND, DEVICE_ACTION_MOVE)) {
+ log_device_debug(device, "Skipping to apply .link settings on '%s' uevent.", device_action_to_string(a));
+
+ r = sd_device_get_sysname(device, ret_name);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get sysname: %m");
+
+ return 0;
+ }
+
+ r = link_config_apply_ethtool_settings(&ctx->ethtool_fd, config, device);
+ if (r < 0)
+ return r;
+
+ r = link_config_apply_rtnl_settings(&ctx->rtnl, config, device);
+ if (r < 0)
+ return r;
+
+ if (a == DEVICE_ACTION_MOVE) {
+ log_device_debug(device, "Skipping to apply Name= and NamePolicy= on '%s' uevent.", device_action_to_string(a));
+
+ r = sd_device_get_sysname(device, &new_name);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get sysname: %m");
+ } else {
+ r = link_config_generate_new_name(ctx, config, device, &new_name);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_config_apply_alternative_names(&ctx->rtnl, config, device, new_name);
+ if (r < 0)
+ return r;
+
+ *ret_name = new_name;
+ return 0;
+}
+
+int link_get_driver(link_config_ctx *ctx, sd_device *device, char **ret) {
+ const char *name;
+ char *driver = NULL;
+ int r;
+
+ r = sd_device_get_sysname(device, &name);
+ if (r < 0)
+ return r;
+
+ r = ethtool_get_driver(&ctx->ethtool_fd, name, &driver);
+ if (r < 0)
+ return r;
+
+ *ret = driver;
+ 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");
+
+static const char* const name_policy_table[_NAMEPOLICY_MAX] = {
+ [NAMEPOLICY_KERNEL] = "kernel",
+ [NAMEPOLICY_KEEP] = "keep",
+ [NAMEPOLICY_DATABASE] = "database",
+ [NAMEPOLICY_ONBOARD] = "onboard",
+ [NAMEPOLICY_SLOT] = "slot",
+ [NAMEPOLICY_PATH] = "path",
+ [NAMEPOLICY_MAC] = "mac",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(name_policy, NamePolicy);
+DEFINE_CONFIG_PARSE_ENUMV(config_parse_name_policy, name_policy, NamePolicy,
+ _NAMEPOLICY_INVALID,
+ "Failed to parse interface name policy");
+
+static const char* const alternative_names_policy_table[_NAMEPOLICY_MAX] = {
+ [NAMEPOLICY_DATABASE] = "database",
+ [NAMEPOLICY_ONBOARD] = "onboard",
+ [NAMEPOLICY_SLOT] = "slot",
+ [NAMEPOLICY_PATH] = "path",
+ [NAMEPOLICY_MAC] = "mac",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(alternative_names_policy, NamePolicy);
+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..eab1849
--- /dev/null
+++ b/src/udev/net/link-config.h
@@ -0,0 +1,98 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-device.h"
+
+#include "condition.h"
+#include "conf-parser.h"
+#include "ethtool-util.h"
+#include "list.h"
+#include "set.h"
+
+typedef struct link_config_ctx link_config_ctx;
+typedef struct link_config link_config;
+
+typedef enum MACAddressPolicy {
+ MAC_ADDRESS_POLICY_PERSISTENT,
+ MAC_ADDRESS_POLICY_RANDOM,
+ MAC_ADDRESS_POLICY_NONE,
+ _MAC_ADDRESS_POLICY_MAX,
+ _MAC_ADDRESS_POLICY_INVALID = -1
+} MACAddressPolicy;
+
+typedef enum NamePolicy {
+ NAMEPOLICY_KERNEL,
+ NAMEPOLICY_KEEP,
+ NAMEPOLICY_DATABASE,
+ NAMEPOLICY_ONBOARD,
+ NAMEPOLICY_SLOT,
+ NAMEPOLICY_PATH,
+ NAMEPOLICY_MAC,
+ _NAMEPOLICY_MAX,
+ _NAMEPOLICY_INVALID = -1
+} NamePolicy;
+
+struct link_config {
+ char *filename;
+
+ Set *match_mac;
+ Set *match_permanent_mac;
+ char **match_path;
+ char **match_driver;
+ char **match_type;
+ char **match_name;
+ char **match_property;
+ LIST_HEAD(Condition, conditions);
+
+ char *description;
+ struct ether_addr *mac;
+ MACAddressPolicy mac_address_policy;
+ NamePolicy *name_policy;
+ NamePolicy *alternative_names_policy;
+ char *name;
+ char **alternative_names;
+ char *alias;
+ uint32_t mtu;
+ uint64_t speed;
+ Duplex duplex;
+ int autonegotiation;
+ uint32_t advertise[N_ADVERTISE];
+ WakeOnLan wol;
+ 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;
+
+ LIST_FIELDS(link_config, links);
+};
+
+int link_config_ctx_new(link_config_ctx **ret);
+void link_config_ctx_free(link_config_ctx *ctx);
+DEFINE_TRIVIAL_CLEANUP_FUNC(link_config_ctx*, link_config_ctx_free);
+
+int link_load_one(link_config_ctx *ctx, const char *filename);
+int link_config_load(link_config_ctx *ctx);
+bool link_config_should_reload(link_config_ctx *ctx);
+
+int link_config_get(link_config_ctx *ctx, sd_device *device, link_config **ret);
+int link_config_apply(link_config_ctx *ctx, const link_config *config, sd_device *device, const char **ret_name);
+int link_get_driver(link_config_ctx *ctx, sd_device *device, char **ret);
+
+const char *name_policy_to_string(NamePolicy p) _const_;
+NamePolicy name_policy_from_string(const char *p) _pure_;
+
+const char *alternative_names_policy_to_string(NamePolicy p) _const_;
+NamePolicy alternative_names_policy_from_string(const char *p) _pure_;
+
+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_mac_address_policy);
+CONFIG_PARSER_PROTOTYPE(config_parse_name_policy);
+CONFIG_PARSER_PROTOTYPE(config_parse_alternative_names_policy);
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..5720256
--- /dev/null
+++ b/src/udev/scsi_id/scsi_id.c
@@ -0,0 +1,595 @@
+/* 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 <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "build.h"
+#include "fd-util.h"
+#include "libudev-util.h"
+#include "scsi_id.h"
+#include "string-util.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' },
+ { "blacklisted", no_argument, NULL, 'b' },
+ { "whitelisted", no_argument, NULL, 'g' },
+ { "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(const char *from, char *to, size_t len) {
+ int type_num;
+ char *eptr;
+ const char *type = "generic";
+
+ type_num = strtoul(from, &eptr, 0);
+ if (eptr != from) {
+ 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:
+ break;
+ }
+ }
+ strscpy(to, len, type);
+}
+
+/*
+ * get_value:
+ *
+ * buf points to an '=' followed by a quoted string ("foo") or a string ending
+ * with a space or ','.
+ *
+ * Return a pointer to the NUL terminated string, returns NULL if no
+ * matches.
+ */
+static char *get_value(char **buffer) {
+ static const char *quote_string = "\"\n";
+ static const char *comma_string = ",\n";
+ char *val;
+ const char *end;
+
+ if (**buffer == '"') {
+ /*
+ * skip leading quote, terminate when quote seen
+ */
+ (*buffer)++;
+ end = quote_string;
+ } else
+ end = comma_string;
+ val = strsep(buffer, end);
+ if (val && end == quote_string)
+ /*
+ * skip trailing quote
+ */
+ (*buffer)++;
+
+ while (isspace(**buffer))
+ (*buffer)++;
+
+ return val;
+}
+
+static int argc_count(char *opts) {
+ int i = 0;
+ while (*opts != '\0')
+ if (*opts++ == ' ')
+ i++;
+ return i;
+}
+
+/*
+ * 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 *buffer = NULL;
+ _cleanup_fclose_ FILE *f;
+ char *buf;
+ char *str1;
+ char *vendor_in, *model_in, *options_in; /* read in from file */
+ int lineno;
+ int c;
+ int retval = 0;
+
+ 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;
+ }
+ }
+
+ /*
+ * Allocate a buffer rather than put it on the stack so we can
+ * keep it around to parse any options (any allocated newargv
+ * points into this buffer for its strings).
+ */
+ buffer = malloc(MAX_BUFFER_LEN);
+ if (!buffer)
+ return log_oom();
+
+ *newargv = NULL;
+ lineno = 0;
+ for (;;) {
+ vendor_in = model_in = options_in = NULL;
+
+ buf = fgets(buffer, MAX_BUFFER_LEN, f);
+ if (!buf)
+ break;
+ lineno++;
+ if (buf[strlen(buffer) - 1] != '\n') {
+ log_error("Config file line %d too long", lineno);
+ break;
+ }
+
+ while (isspace(*buf))
+ buf++;
+
+ /* blank or all whitespace line */
+ if (*buf == '\0')
+ continue;
+
+ /* comment line */
+ if (*buf == '#')
+ continue;
+
+ str1 = strsep(&buf, "=");
+ if (str1 && strcaseeq(str1, "VENDOR")) {
+ str1 = get_value(&buf);
+ if (!str1) {
+ retval = log_oom();
+ break;
+ }
+ vendor_in = str1;
+
+ str1 = strsep(&buf, "=");
+ if (str1 && strcaseeq(str1, "MODEL")) {
+ str1 = get_value(&buf);
+ if (!str1) {
+ retval = log_oom();
+ break;
+ }
+ model_in = str1;
+ str1 = strsep(&buf, "=");
+ }
+ }
+
+ if (str1 && strcaseeq(str1, "OPTIONS")) {
+ str1 = get_value(&buf);
+ if (!str1) {
+ retval = log_oom();
+ break;
+ }
+ options_in = str1;
+ }
+
+ /*
+ * Only allow: [vendor=foo[,model=bar]]options=stuff
+ */
+ if (!options_in || (!vendor_in && model_in)) {
+ log_error("Error parsing config file line %d '%s'", lineno, buffer);
+ retval = -1;
+ break;
+ }
+ 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;
+ }
+ }
+
+ if (retval == 0) {
+ if (vendor_in != NULL || model_in != NULL ||
+ options_in != NULL) {
+ /*
+ * Something matched. Allocate newargv, and store
+ * values found in options_in.
+ */
+ strcpy(buffer, options_in);
+ c = argc_count(buffer) + 2;
+ *newargv = calloc(c, sizeof(**newargv));
+ if (!*newargv)
+ retval = log_oom();
+ else {
+ *argc = c;
+ c = 0;
+ /*
+ * argv[0] at 0 is skipped by getopt, but
+ * store the buffer address there for
+ * later freeing
+ */
+ (*newargv)[c] = buffer;
+ for (c = 1; c < *argc; c++)
+ (*newargv)[c] = strsep(&buffer, " \t");
+ buffer = NULL;
+ }
+ } else {
+ /* No matches */
+ retval = 1;
+ }
+ }
+ return retval;
+}
+
+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 --blacklisted Treat device as blacklisted\n"
+ " -g --whitelisted Treat device as whitelisted\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':
+ printf("%s\n", GIT_VERSION);
+ exit(EXIT_SUCCESS);
+
+ case 'x':
+ export = true;
+ break;
+
+ case '?':
+ return -1;
+
+ default:
+ assert_not_reached("Unknown option");
+ }
+
+ 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) {
+ int retval;
+ int newargc;
+ char **newargv = NULL;
+ 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, option);
+ retval = -1;
+ break;
+ }
+ }
+
+ if (newargv) {
+ free(newargv[0]);
+ free(newargv);
+ }
+ 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;
+
+ udev_util_encode_string(dev_scsi->vendor, vendor_enc_str, sizeof(vendor_enc_str));
+ udev_util_encode_string(dev_scsi->model, model_enc_str, sizeof(model_enc_str));
+
+ util_replace_whitespace(dev_scsi->vendor, vendor_str, sizeof(vendor_str)-1);
+ util_replace_chars(vendor_str, NULL);
+ util_replace_whitespace(dev_scsi->model, model_str, sizeof(model_str)-1);
+ util_replace_chars(model_str, NULL);
+ set_type(dev_scsi->type, type_str, sizeof(type_str));
+ util_replace_whitespace(dev_scsi->revision, revision_str, sizeof(revision_str)-1);
+ util_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') {
+ util_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1);
+ util_replace_chars(serial_str, NULL);
+ printf("ID_SERIAL=%s\n", serial_str);
+ util_replace_whitespace(dev_scsi.serial_short, serial_str, sizeof(serial_str)-1);
+ util_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];
+
+ util_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1);
+ util_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) {
+ int retval = 0;
+ char maj_min_dev[MAX_PATH_LEN];
+ int newargc;
+ char **newargv = NULL;
+
+ 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:
+ if (newargv) {
+ free(newargv[0]);
+ free(newargv);
+ }
+ 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..2fe64f4
--- /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 type[33];
+ char kernel[64];
+ char serial[MAX_SERIAL_LEN];
+ char serial_short[MAX_SERIAL_LEN];
+ 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..4fe7254
--- /dev/null
+++ b/src/udev/scsi_id/scsi_serial.c
@@ -0,0 +1,893 @@
+/* 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 "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 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_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 (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;
+ int code;
+ int sense_class;
+ int sense_key;
+ int 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 black 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] & 0x0f);
+ else
+ log_debug("%s: sense = %2x %2x",
+ dev_scsi->kernel, sense_buffer[0], sense_buffer[2]);
+ log_debug("%s: non-extended sense class %d 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",
+ __FUNCTION__);
+
+ 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",
+ __FUNCTION__);
+
+ 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 %d 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;
+
+ 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);
+ 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;
+ }
+ sprintf(dev_scsi->kernel,"%d:%d", major(statbuf.st_rdev),
+ minor(statbuf.st_rdev));
+
+ 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';
+ sprintf(dev_scsi->type,"%x", 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 = -1;
+ int cnt;
+ int ind;
+ int retval;
+
+ memzero(dev_scsi->serial, len);
+ initialize_srand();
+ for (cnt = 20; cnt > 0; cnt--) {
+ struct timespec duration;
+
+ fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (fd >= 0 || errno != EBUSY)
+ break;
+ duration.tv_sec = 0;
+ duration.tv_nsec = (200 * 1000 * 1000) + (rand() % 100 * 1000 * 1000);
+ nanosleep(&duration, NULL);
+ }
+ 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/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c
new file mode 100644
index 0000000..3f64548
--- /dev/null
+++ b/src/udev/udev-builtin-blkid.c
@@ -0,0 +1,317 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * probe disks for filesystems and partitions
+ *
+ * Copyright © 2011 Karel Zak <kzak@redhat.com>
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "blkid-util.h"
+#include "device-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 "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 (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);
+ }
+}
+
+static int find_gpt_root(sd_device *dev, blkid_probe pr, bool test) {
+
+#if defined(GPT_ROOT_NATIVE) && ENABLE_EFI
+
+ _cleanup_free_ char *root_id = NULL;
+ bool found_esp = false;
+ blkid_partlist pl;
+ int i, nvals, r;
+
+ assert(pr);
+
+ /* Iterate through the partitions on this disk, and see if the
+ * EFI ESP we booted from is on it. If so, find the first root
+ * disk, and add a property indicating its partition UUID. */
+
+ errno = 0;
+ pl = blkid_probe_get_partitions(pr);
+ if (!pl)
+ return errno_or_else(ENOMEM);
+
+ nvals = blkid_partlist_numof_partitions(pl);
+ for (i = 0; i < nvals; i++) {
+ blkid_partition pp;
+ const char *stype, *sid;
+ sd_id128_t type;
+
+ pp = blkid_partlist_get_partition(pl, i);
+ if (!pp)
+ continue;
+
+ sid = blkid_partition_get_uuid(pp);
+ if (!sid)
+ continue;
+
+ stype = blkid_partition_get_type_string(pp);
+ if (!stype)
+ continue;
+
+ if (sd_id128_from_string(stype, &type) < 0)
+ continue;
+
+ if (sd_id128_equal(type, GPT_ESP)) {
+ sd_id128_t id, esp;
+
+ /* We found an ESP, let's see if it matches
+ * the ESP we booted from. */
+
+ if (sd_id128_from_string(sid, &id) < 0)
+ continue;
+
+ r = efi_loader_get_device_part_uuid(&esp);
+ if (r < 0)
+ return r;
+
+ if (sd_id128_equal(id, esp))
+ found_esp = true;
+
+ } else if (sd_id128_equal(type, GPT_ROOT_NATIVE)) {
+ unsigned long long flags;
+
+ flags = blkid_partition_get_flags(pp);
+ if (flags & GPT_FLAG_NO_AUTO)
+ continue;
+
+ /* We found a suitable root partition, let's
+ * remember the first one. */
+
+ if (!root_id) {
+ root_id = strdup(sid);
+ if (!root_id)
+ return -ENOMEM;
+ }
+ }
+ }
+
+ /* We found the ESP on this disk, and also found a root
+ * partition, nice! Let's export its UUID */
+ if (found_esp && root_id)
+ udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT_UUID", 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 builtin_blkid(sd_device *dev, int argc, char *argv[], bool test) {
+ const char *devnode, *root_partition = NULL, *data, *name;
+ _cleanup_(blkid_free_probep) blkid_probe pr = NULL;
+ bool noraid = false, is_gpt = false;
+ _cleanup_close_ int fd = -1;
+ int64_t offset = 0;
+ int nvals, i, r;
+
+ static const struct option options[] = {
+ { "offset", required_argument, NULL, 'o' },
+ { "noraid", no_argument, NULL, 'R' },
+ {}
+ };
+
+ for (;;) {
+ int option;
+
+ option = getopt_long(argc, argv, "o:R", options, NULL);
+ if (option == -1)
+ break;
+
+ switch (option) {
+ 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(ERANGE), "Invalid offset %"PRIi64": %m", offset);
+ break;
+ case 'R':
+ noraid = true;
+ break;
+ }
+ }
+
+ errno = 0;
+ pr = blkid_new_probe();
+ if (!pr)
+ return log_device_debug_errno(dev, errno > 0 ? errno : ENOMEM, "Failed to create blkid prober: %m");
+
+ blkid_probe_set_superblocks_flags(pr,
+ BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID |
+ BLKID_SUBLKS_TYPE | BLKID_SUBLKS_SECTYPE |
+ 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 = open(devnode, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
+ if (fd < 0)
+ return log_device_debug_errno(dev, errno, "Failed to open block device %s: %m", devnode);
+
+ errno = 0;
+ r = blkid_probe_set_device(pr, fd, offset, 0);
+ if (r < 0)
+ return log_device_debug_errno(dev, errno > 0 ? errno : 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;
+ nvals = blkid_probe_numof_values(pr);
+ if (nvals < 0)
+ return log_device_debug_errno(dev, errno > 0 ? errno : ENOMEM, "Failed to get number of probed values: %m");
+
+ for (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);
+
+ 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..9079d1b
--- /dev/null
+++ b/src/udev/udev-builtin-btrfs.c
@@ -0,0 +1,40 @@
+/* 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 "fd-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+#include "util.h"
+
+static int builtin_btrfs(sd_device *dev, int argc, char *argv[], bool test) {
+ struct btrfs_ioctl_vol_args args = {};
+ _cleanup_close_ int fd = -1;
+ 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);
+ if (fd < 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..7883518
--- /dev/null
+++ b/src/udev/udev-builtin-hwdb.c
@@ -0,0 +1,221 @@
+/* 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);
+
+ 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(sd_device *dev, 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;
+ 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_validate(void) {
+ return hwdb_validate(hwdb);
+}
+
+const UdevBuiltin udev_builtin_hwdb = {
+ .name = "hwdb",
+ .cmd = builtin_hwdb,
+ .init = builtin_hwdb_init,
+ .exit = builtin_hwdb_exit,
+ .validate = builtin_hwdb_validate,
+ .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..6a4911c
--- /dev/null
+++ b/src/udev/udev-builtin-input_id.c
@@ -0,0 +1,395 @@
+/* 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 <stdlib.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"
+#include "util.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, const char *devpath, bool test) {
+ char width[DECIMAL_STR_MAX(int)], height[DECIMAL_STR_MAX(int)];
+ struct input_absinfo xabsinfo = {}, yabsinfo = {};
+ _cleanup_close_ int fd = -1;
+
+ fd = open(devpath, O_RDONLY|O_CLOEXEC);
+ 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;
+
+ 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, ' ')) != NULL) {
+ val = strtoul(word+1, NULL, 16);
+ 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;
+ }
+ val = strtoul (text, NULL, 16);
+ 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) {
+ /* printf pattern with the right unsigned long number of hex chars */
+ xsprintf(text, " bit %%4u: %%0%zulX\n",
+ 2 * sizeof(unsigned long));
+ log_device_debug(pdev, "%s decoded bit map:", attr);
+ val = bitmask_size / sizeof (unsigned long);
+ /* skip over leading zeros */
+ while (bitmask[val-1] == 0 && val > 0)
+ --val;
+ for (i = 0; i < val; ++i) {
+ DISABLE_WARNING_FORMAT_NONLITERAL;
+ log_device_debug(pdev, text, i * BITS_PER_LONG, bitmask[i]);
+ REENABLE_WARNING;
+ }
+ }
+}
+
+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) {
+ int button, axis;
+ bool has_abs_coordinates = false;
+ bool has_rel_coordinates = false;
+ bool has_mt_coordinates = false;
+ bool has_joystick_axes_or_buttons = false;
+ 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_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;
+
+ 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 (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) && has_stylus && !has_pen;
+
+ /* 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 (button = BTN_JOYSTICK; button < BTN_DIGI && !has_joystick_axes_or_buttons; button++)
+ has_joystick_axes_or_buttons = test_bit(button, bitmask_key);
+ for (button = BTN_TRIGGER_HAPPY1; button <= BTN_TRIGGER_HAPPY40 && !has_joystick_axes_or_buttons; button++)
+ has_joystick_axes_or_buttons = test_bit(button, bitmask_key);
+ for (button = BTN_DPAD_UP; button <= BTN_DPAD_RIGHT && !has_joystick_axes_or_buttons; button++)
+ has_joystick_axes_or_buttons = test_bit(button, bitmask_key);
+ }
+ for (axis = ABS_RX; axis < ABS_PRESSURE && !has_joystick_axes_or_buttons; axis++)
+ has_joystick_axes_or_buttons = test_bit(axis, bitmask_abs);
+
+ 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_mouse = true;
+ else if (has_touch || is_direct)
+ is_touchscreen = true;
+ else if (has_joystick_axes_or_buttons)
+ is_joystick = true;
+ } else if (has_joystick_axes_or_buttons)
+ 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 (!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;
+
+ if (is_pointing_stick)
+ udev_builtin_add_property(dev, test, "ID_INPUT_POINTINGSTICK", "1");
+ if (is_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_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) {
+ unsigned i;
+ unsigned long found;
+ unsigned long mask;
+ bool ret = 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_* */
+ found = 0;
+ for (i = 0; i < BTN_MISC/BITS_PER_LONG; ++i) {
+ found |= bitmask_key[i];
+ log_device_debug(dev, "test_key: checking bit block %lu for any keys; found=%i", (unsigned long)i*BITS_PER_LONG, found > 0);
+ }
+ /* If there are no keys in the lower block, check the higher blocks */
+ if (!found) {
+ unsigned block;
+ for (block = 0; block < (sizeof(high_key_blocks) / sizeof(struct range)); ++block) {
+ for (i = high_key_blocks[block].start; i < high_key_blocks[block].end; ++i) {
+ if (test_bit(i, bitmask_key)) {
+ log_device_debug(dev, "test_key: Found key %x in high block", i);
+ found = 1;
+ break;
+ }
+ }
+ }
+ }
+
+ if (found > 0) {
+ udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1");
+ ret = true;
+ }
+
+ /* 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 */
+ mask = 0xFFFFFFFE;
+ if (FLAGS_SET(bitmask_key[0], mask)) {
+ udev_builtin_add_property(dev, test, "ID_INPUT_KEYBOARD", "1");
+ ret = true;
+ }
+
+ return ret;
+}
+
+static int builtin_input_id(sd_device *dev, int argc, char *argv[], bool test) {
+ sd_device *pdev;
+ 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, *devnode;
+ bool is_pointer;
+ bool is_key;
+
+ assert(dev);
+
+ /* 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_devname(dev, &devnode) >= 0 &&
+ sd_device_get_sysname(dev, &sysname) >= 0 &&
+ startswith(sysname, "event"))
+ extract_info(dev, devnode, 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..cd766a8
--- /dev/null
+++ b/src/udev/udev-builtin-keyboard.c
@@ -0,0 +1,254 @@
+/* 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;
+ char *endptr;
+ const struct key_name *k;
+ unsigned keycode_num;
+
+ /* 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 */
+ keycode_num = strtoul(keycode, &endptr, 0);
+ if (endptr[0] !='\0')
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Failed to parse key identifier '%s'", keycode);
+ }
+
+ map.scan = scancode;
+ map.key = keycode_num;
+
+ log_device_debug(dev, "keyboard: mapping scan code %d (0x%x) to key code %d (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 %d: %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(sd_device *dev, int argc, char *argv[], bool test) {
+ unsigned release[1024];
+ unsigned release_count = 0;
+ _cleanup_close_ int fd = -1;
+ const char *node, *key, *value;
+ 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) {
+ char *endptr;
+
+ if (startswith(key, "KEYBOARD_KEY_")) {
+ const char *keycode = value;
+ unsigned scancode;
+
+ /* KEYBOARD_KEY_<hex scan code>=<key identifier string> */
+ scancode = strtoul(key + 13, &endptr, 16);
+ if (endptr[0] != '\0') {
+ log_device_warning(dev, "Failed to parse scan code from \"%s\", ignoring", 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 = open(node, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return log_device_error_errno(dev, errno, "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> */
+ evcode = strtoul(key + 10, &endptr, 16);
+ if (endptr[0] != '\0') {
+ log_device_warning(dev, "Failed to parse EV_ABS code from \"%s\", ignoring", key);
+ continue;
+ }
+
+ if (fd < 0) {
+ fd = open(node, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return log_device_error_errno(dev, errno, "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 scan code to key mapping",
+};
diff --git a/src/udev/udev-builtin-kmod.c b/src/udev/udev-builtin-kmod.c
new file mode 100644
index 0000000..3be8bd5
--- /dev/null
+++ b/src/udev/udev-builtin-kmod.c
@@ -0,0 +1,76 @@
+/* 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 "module-util.h"
+#include "string-util.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(sd_device *dev, int argc, char *argv[], bool test) {
+ int i;
+
+ if (!ctx)
+ return 0;
+
+ if (argc < 3 || !streq(argv[1], "load"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: expected: load <module>", argv[0]);
+
+ for (i = 2; argv[i]; i++)
+ (void) module_load_and_warn(ctx, argv[i], 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("Load 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 module index");
+ ctx = kmod_unref(ctx);
+}
+
+/* called every couple of seconds during event activity; 'true' if config has changed */
+static bool builtin_kmod_validate(void) {
+ log_debug("Validate module index");
+ if (!ctx)
+ return false;
+ return (kmod_validate_resources(ctx) != KMOD_RESOURCES_OK);
+}
+
+const UdevBuiltin udev_builtin_kmod = {
+ .name = "kmod",
+ .cmd = builtin_kmod,
+ .init = builtin_kmod_init,
+ .exit = builtin_kmod_exit,
+ .validate = builtin_kmod_validate,
+ .help = "Kernel module loader",
+ .run_once = false,
+};
diff --git a/src/udev/udev-builtin-net_id.c b/src/udev/udev-builtin-net_id.c
new file mode 100644
index 0000000..d06a8c7
--- /dev/null
+++ b/src/udev/udev-builtin-net_id.c
@@ -0,0 +1,961 @@
+/* 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 <net/if_arp.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <linux/if.h>
+#include <linux/pci_regs.h>
+
+#include "alloc-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-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_INDEX_MAX (16*1024-1)
+
+enum netname_type{
+ NET_UNDEF,
+ NET_PCI,
+ NET_USB,
+ NET_BCMA,
+ NET_VIRTIO,
+ NET_CCW,
+ NET_VIO,
+ NET_PLATFORM,
+ NET_NETDEVSIM,
+};
+
+struct netnames {
+ enum netname_type type;
+
+ uint8_t mac[6];
+ bool mac_valid;
+
+ sd_device *pcidev;
+ char pci_slot[ALTIFNAMSIZ];
+ char pci_path[ALTIFNAMSIZ];
+ char pci_onboard[ALTIFNAMSIZ];
+ const char *pci_onboard_label;
+
+ char usb_ports[ALTIFNAMSIZ];
+ char bcma_core[ALTIFNAMSIZ];
+ char ccw_busid[ALTIFNAMSIZ];
+ char vio_slot[ALTIFNAMSIZ];
+ char platform_path[ALTIFNAMSIZ];
+ char netdevsim_path[ALTIFNAMSIZ];
+};
+
+struct virtfn_info {
+ sd_device *physfn_pcidev;
+ char suffix[ALTIFNAMSIZ];
+};
+
+/* skip intermediate virtio devices */
+static sd_device *skip_virtio(sd_device *dev) {
+ sd_device *parent;
+
+ /* 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 */
+ for (parent = dev; parent; ) {
+ const char *subsystem;
+
+ if (sd_device_get_subsystem(parent, &subsystem) < 0)
+ break;
+
+ if (!streq(subsystem, "virtio"))
+ break;
+
+ if (sd_device_get_parent(parent, &parent) < 0)
+ return NULL;
+ }
+
+ return parent;
+}
+
+static int get_virtfn_info(sd_device *dev, struct netnames *names, struct virtfn_info *ret) {
+ _cleanup_(sd_device_unrefp) sd_device *physfn_pcidev = NULL;
+ const char *physfn_link_file, *syspath;
+ _cleanup_free_ char *physfn_pci_syspath = NULL;
+ _cleanup_free_ char *virtfn_pci_syspath = NULL;
+ struct dirent *dent;
+ _cleanup_closedir_ DIR *dir = NULL;
+ char suffix[ALTIFNAMSIZ];
+ int r;
+
+ assert(dev);
+ assert(names);
+ assert(ret);
+
+ r = sd_device_get_syspath(names->pcidev, &syspath);
+ if (r < 0)
+ return r;
+
+ /* Check if this is a virtual function. */
+ physfn_link_file = strjoina(syspath, "/physfn");
+ r = chase_symlinks(physfn_link_file, NULL, 0, &physfn_pci_syspath, NULL);
+ if (r < 0)
+ return r;
+
+ /* Get physical function's pci device. */
+ r = sd_device_new_from_syspath(&physfn_pcidev, physfn_pci_syspath);
+ if (r < 0)
+ return r;
+
+ /* Find the virtual function number by finding the right virtfn link. */
+ dir = opendir(physfn_pci_syspath);
+ if (!dir)
+ return -errno;
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ _cleanup_free_ char *virtfn_link_file = NULL;
+
+ if (!startswith(dent->d_name, "virtfn"))
+ continue;
+
+ virtfn_link_file = path_join(physfn_pci_syspath, dent->d_name);
+ if (!virtfn_link_file)
+ return -ENOMEM;
+
+ if (chase_symlinks(virtfn_link_file, NULL, 0, &virtfn_pci_syspath, NULL) < 0)
+ continue;
+
+ if (streq(syspath, virtfn_pci_syspath)) {
+ if (!snprintf_ok(suffix, sizeof(suffix), "v%s", &dent->d_name[6]))
+ return -ENOENT;
+
+ break;
+ }
+ }
+ if (isempty(suffix))
+ return -ENOENT;
+
+ ret->physfn_pcidev = TAKE_PTR(physfn_pcidev);
+ strncpy(ret->suffix, suffix, sizeof(ret->suffix));
+
+ return 0;
+}
+
+/* retrieve on-board index number and label from firmware */
+static int dev_pci_onboard(sd_device *dev, struct netnames *names) {
+ unsigned long idx, dev_port = 0;
+ const char *attr, *port_name = NULL;
+ size_t l;
+ char *s;
+ int r;
+
+ /* ACPI _DSM — device specific method for naming a PCI or PCI Express device */
+ if (sd_device_get_sysattr_value(names->pcidev, "acpi_index", &attr) < 0) {
+ /* SMBIOS type 41 — Onboard Devices Extended Information */
+ r = sd_device_get_sysattr_value(names->pcidev, "index", &attr);
+ if (r < 0)
+ return r;
+ }
+
+ r = safe_atolu(attr, &idx);
+ if (r < 0)
+ return r;
+ if (idx == 0 && !naming_scheme_has(NAMING_ZERO_ACPI_INDEX))
+ return -EINVAL;
+
+ /* 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. */
+ if (idx > ONBOARD_INDEX_MAX)
+ return -ENOENT;
+
+ /* kernel provided port index for multiple ports on a single PCI function */
+ if (sd_device_get_sysattr_value(dev, "dev_port", &attr) >= 0)
+ dev_port = strtoul(attr, NULL, 10);
+
+ /* kernel provided front panel port name for multiple port PCI device */
+ (void) sd_device_get_sysattr_value(dev, "phys_port_name", &port_name);
+
+ s = names->pci_onboard;
+ l = sizeof(names->pci_onboard);
+ l = strpcpyf(&s, l, "o%lu", idx);
+ if (port_name)
+ l = strpcpyf(&s, l, "n%s", port_name);
+ else if (dev_port > 0)
+ l = strpcpyf(&s, l, "d%lu", dev_port);
+ if (l == 0)
+ names->pci_onboard[0] = '\0';
+
+ if (sd_device_get_sysattr_value(names->pcidev, "label", &names->pci_onboard_label) < 0)
+ names->pci_onboard_label = NULL;
+
+ return 0;
+}
+
+/* read the 256 bytes PCI configuration space to check the multi-function bit */
+static bool is_pci_multifunction(sd_device *dev) {
+ _cleanup_close_ int fd = -1;
+ const char *filename, *syspath;
+ uint8_t config[64];
+
+ if (sd_device_get_syspath(dev, &syspath) < 0)
+ return false;
+
+ filename = strjoina(syspath, "/config");
+ fd = open(filename, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return false;
+ if (read(fd, &config, sizeof(config)) != sizeof(config))
+ return false;
+
+ /* bit 0-6 header type, bit 7 multi/single function device */
+ return config[PCI_HEADER_TYPE] & 0x80;
+}
+
+static bool is_pci_ari_enabled(sd_device *dev) {
+ const char *a;
+
+ if (sd_device_get_sysattr_value(dev, "ari_enabled", &a) < 0)
+ return false;
+
+ return streq(a, "1");
+}
+
+static bool is_pci_bridge(sd_device *dev) {
+ const char *v, *p;
+
+ 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 */
+ return strneq(p + 2, "04", 2);
+}
+
+static int dev_pci_slot(sd_device *dev, struct netnames *names) {
+ unsigned long dev_port = 0;
+ unsigned domain, bus, slot, func;
+ int hotplug_slot = -1;
+ size_t l;
+ char *s;
+ const char *sysname, *attr, *port_name = NULL, *syspath;
+ _cleanup_(sd_device_unrefp) sd_device *pci = NULL;
+ sd_device *hotplug_slot_dev;
+ char slots[PATH_MAX];
+ _cleanup_closedir_ DIR *dir = NULL;
+ struct dirent *dent;
+ int r;
+
+ r = sd_device_get_sysname(names->pcidev, &sysname);
+ if (r < 0)
+ return r;
+
+ if (sscanf(sysname, "%x:%x:%x.%u", &domain, &bus, &slot, &func) != 4)
+ return -ENOENT;
+
+ if (naming_scheme_has(NAMING_NPAR_ARI) &&
+ is_pci_ari_enabled(names->pcidev))
+ /* 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;
+
+ /* kernel provided port index for multiple ports on a single PCI function */
+ if (sd_device_get_sysattr_value(dev, "dev_port", &attr) >= 0) {
+ dev_port = strtoul(attr, NULL, 10);
+ /* 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 (dev_port == 0 &&
+ sd_device_get_sysattr_value(dev, "type", &attr) >= 0) {
+ unsigned long type;
+
+ type = strtoul(attr, NULL, 10);
+ if (type == ARPHRD_INFINIBAND &&
+ sd_device_get_sysattr_value(dev, "dev_id", &attr) >= 0)
+ dev_port = strtoul(attr, NULL, 16);
+ }
+ }
+
+ /* kernel provided front panel port name for multi-port PCI device */
+ (void) sd_device_get_sysattr_value(dev, "phys_port_name", &port_name);
+
+ /* compose a name based on the raw kernel's PCI bus, slot numbers */
+ s = names->pci_path;
+ l = sizeof(names->pci_path);
+ if (domain > 0)
+ l = strpcpyf(&s, l, "P%u", domain);
+ l = strpcpyf(&s, l, "p%us%u", bus, slot);
+ if (func > 0 || is_pci_multifunction(names->pcidev))
+ l = strpcpyf(&s, l, "f%u", func);
+ if (port_name)
+ l = strpcpyf(&s, l, "n%s", port_name);
+ else if (dev_port > 0)
+ l = strpcpyf(&s, l, "d%lu", dev_port);
+ if (l == 0)
+ names->pci_path[0] = '\0';
+
+ /* ACPI _SUN — slot user number */
+ r = sd_device_new_from_subsystem_sysname(&pci, "subsystem", "pci");
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_syspath(pci, &syspath);
+ if (r < 0)
+ return r;
+ if (!snprintf_ok(slots, sizeof slots, "%s/slots", syspath))
+ return -ENAMETOOLONG;
+
+ dir = opendir(slots);
+ if (!dir)
+ return -errno;
+
+ hotplug_slot_dev = names->pcidev;
+ while (hotplug_slot_dev) {
+ if (sd_device_get_sysname(hotplug_slot_dev, &sysname) < 0)
+ continue;
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ int i;
+ char str[PATH_MAX];
+ _cleanup_free_ char *address = NULL;
+
+ if (dot_or_dot_dot(dent->d_name))
+ continue;
+
+ r = safe_atoi(dent->d_name, &i);
+ if (r < 0 || i <= 0)
+ continue;
+
+ /* match slot address with device by stripping the function */
+ if (snprintf_ok(str, sizeof str, "%s/%s/address", slots, dent->d_name) &&
+ read_one_line_file(str, &address) >= 0 &&
+ startswith(sysname, address)) {
+ hotplug_slot = i;
+
+ /* 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(hotplug_slot_dev))
+ hotplug_slot = 0;
+
+ break;
+ }
+ }
+ if (hotplug_slot >= 0)
+ break;
+ if (sd_device_get_parent_with_subsystem_devtype(hotplug_slot_dev, "pci", NULL, &hotplug_slot_dev) < 0)
+ break;
+ rewinddir(dir);
+ }
+
+ if (hotplug_slot > 0) {
+ s = names->pci_slot;
+ l = sizeof(names->pci_slot);
+ if (domain > 0)
+ l = strpcpyf(&s, l, "P%d", domain);
+ l = strpcpyf(&s, l, "s%d", hotplug_slot);
+ if (func > 0 || is_pci_multifunction(names->pcidev))
+ l = strpcpyf(&s, l, "f%d", func);
+ if (port_name)
+ l = strpcpyf(&s, l, "n%s", port_name);
+ else if (dev_port > 0)
+ l = strpcpyf(&s, l, "d%lu", dev_port);
+ if (l == 0)
+ names->pci_slot[0] = '\0';
+ }
+
+ return 0;
+}
+
+static int names_vio(sd_device *dev, struct netnames *names) {
+ sd_device *parent;
+ unsigned busid, slotid, ethid;
+ const char *syspath, *subsystem;
+ int r;
+
+ /* check if our direct parent is a VIO device with no other bus in-between */
+ r = sd_device_get_parent(dev, &parent);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_subsystem(parent, &subsystem);
+ if (r < 0)
+ return r;
+ if (!streq("vio", subsystem))
+ return -ENOENT;
+
+ /* 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 = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ if (sscanf(syspath, "/sys/devices/vio/%4x%4x/net/eth%u", &busid, &slotid, &ethid) != 3)
+ return -EINVAL;
+
+ xsprintf(names->vio_slot, "v%u", slotid);
+ names->type = NET_VIO;
+ return 0;
+}
+
+#define _PLATFORM_TEST "/sys/devices/platform/vvvvPPPP"
+#define _PLATFORM_PATTERN4 "/sys/devices/platform/%4s%4x:%2x/net/eth%u"
+#define _PLATFORM_PATTERN3 "/sys/devices/platform/%3s%4x:%2x/net/eth%u"
+
+static int names_platform(sd_device *dev, struct netnames *names, bool test) {
+ sd_device *parent;
+ char vendor[5];
+ unsigned model, instance, ethid;
+ const char *syspath, *pattern, *validchars, *subsystem;
+ int r;
+
+ /* check if our direct parent is a platform device with no other bus in-between */
+ r = sd_device_get_parent(dev, &parent);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_subsystem(parent, &subsystem);
+ if (r < 0)
+ return r;
+
+ if (!streq("platform", subsystem))
+ return -ENOENT;
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ /* syspath is too short, to have a valid ACPI instance */
+ if (strlen(syspath) < sizeof _PLATFORM_TEST)
+ return -EINVAL;
+
+ /* Vendor ID can be either PNP ID (3 chars A-Z) or ACPI ID (4 chars A-Z and numerals) */
+ if (syspath[sizeof _PLATFORM_TEST - 1] == ':') {
+ pattern = _PLATFORM_PATTERN4;
+ validchars = UPPERCASE_LETTERS DIGITS;
+ } else {
+ pattern = _PLATFORM_PATTERN3;
+ validchars = UPPERCASE_LETTERS;
+ }
+
+ /* 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 hexdecimal model number : instance id.
+ */
+
+ DISABLE_WARNING_FORMAT_NONLITERAL;
+ if (sscanf(syspath, pattern, vendor, &model, &instance, &ethid) != 4)
+ return -EINVAL;
+ REENABLE_WARNING;
+
+ if (!in_charset(vendor, validchars))
+ return -ENOENT;
+
+ ascii_strlower(vendor);
+
+ xsprintf(names->platform_path, "a%s%xi%u", vendor, model, instance);
+ names->type = NET_PLATFORM;
+ return 0;
+}
+
+static int names_pci(sd_device *dev, struct netnames *names) {
+ sd_device *parent;
+ struct netnames vf_names = {};
+ struct virtfn_info vf_info = {};
+ const char *subsystem;
+ int r;
+
+ assert(dev);
+ assert(names);
+
+ r = sd_device_get_parent(dev, &parent);
+ if (r < 0)
+ return r;
+ /* skip virtio subsystem if present */
+ parent = skip_virtio(parent);
+
+ if (!parent)
+ return -ENOENT;
+
+ /* check if our direct parent is a PCI device with no other bus in-between */
+ if (sd_device_get_subsystem(parent, &subsystem) >= 0 &&
+ streq("pci", subsystem)) {
+ names->type = NET_PCI;
+ names->pcidev = parent;
+ } else {
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "pci", NULL, &names->pcidev);
+ if (r < 0)
+ return r;
+ }
+
+ if (naming_scheme_has(NAMING_SR_IOV_V) &&
+ get_virtfn_info(dev, names, &vf_info) >= 0) {
+ /* If this is an SR-IOV virtual device, get base name using physical device and add virtfn suffix. */
+ vf_names.pcidev = vf_info.physfn_pcidev;
+ dev_pci_onboard(dev, &vf_names);
+ dev_pci_slot(dev, &vf_names);
+ if (vf_names.pci_onboard[0])
+ if (strlen(vf_names.pci_onboard) + strlen(vf_info.suffix) < sizeof(names->pci_onboard))
+ strscpyl(names->pci_onboard, sizeof(names->pci_onboard),
+ vf_names.pci_onboard, vf_info.suffix, NULL);
+ if (vf_names.pci_slot[0])
+ if (strlen(vf_names.pci_slot) + strlen(vf_info.suffix) < sizeof(names->pci_slot))
+ strscpyl(names->pci_slot, sizeof(names->pci_slot),
+ vf_names.pci_slot, vf_info.suffix, NULL);
+ if (vf_names.pci_path[0])
+ if (strlen(vf_names.pci_path) + strlen(vf_info.suffix) < sizeof(names->pci_path))
+ strscpyl(names->pci_path, sizeof(names->pci_path),
+ vf_names.pci_path, vf_info.suffix, NULL);
+ sd_device_unref(vf_info.physfn_pcidev);
+ } else {
+ dev_pci_onboard(dev, names);
+ dev_pci_slot(dev, names);
+ }
+
+ return 0;
+}
+
+static int names_usb(sd_device *dev, struct netnames *names) {
+ sd_device *usbdev;
+ char name[256], *ports, *config, *interf, *s;
+ const char *sysname;
+ size_t l;
+ int r;
+
+ assert(dev);
+ assert(names);
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &usbdev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_sysname(usbdev, &sysname);
+ if (r < 0)
+ return r;
+
+ /* get USB port number chain, configuration, interface */
+ strscpy(name, sizeof(name), sysname);
+ s = strchr(name, '-');
+ if (!s)
+ return -EINVAL;
+ ports = s+1;
+
+ s = strchr(ports, ':');
+ if (!s)
+ return -EINVAL;
+ s[0] = '\0';
+ config = s+1;
+
+ s = strchr(config, '.');
+ if (!s)
+ return -EINVAL;
+ s[0] = '\0';
+ interf = s+1;
+
+ /* prefix every port number in the chain with "u" */
+ s = ports;
+ while ((s = strchr(s, '.')))
+ s[0] = 'u';
+ s = names->usb_ports;
+ l = strpcpyl(&s, sizeof(names->usb_ports), "u", ports, NULL);
+
+ /* append USB config number, suppress the common config == 1 */
+ if (!streq(config, "1"))
+ l = strpcpyl(&s, sizeof(names->usb_ports), "c", config, NULL);
+
+ /* append USB interface number, suppress the interface == 0 */
+ if (!streq(interf, "0"))
+ l = strpcpyl(&s, sizeof(names->usb_ports), "i", interf, NULL);
+ if (l == 0)
+ return -ENAMETOOLONG;
+
+ names->type = NET_USB;
+ return 0;
+}
+
+static int names_bcma(sd_device *dev, struct netnames *names) {
+ sd_device *bcmadev;
+ unsigned core;
+ const char *sysname;
+ int r;
+
+ assert(dev);
+ assert(names);
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "bcma", NULL, &bcmadev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_sysname(bcmadev, &sysname);
+ if (r < 0)
+ return r;
+
+ /* bus num:core num */
+ if (sscanf(sysname, "bcma%*u:%u", &core) != 1)
+ return -EINVAL;
+ /* suppress the common core == 0 */
+ if (core > 0)
+ xsprintf(names->bcma_core, "b%u", core);
+
+ names->type = NET_BCMA;
+ return 0;
+}
+
+static int names_ccw(sd_device *dev, struct netnames *names) {
+ sd_device *cdev;
+ const char *bus_id, *subsys;
+ size_t bus_id_len;
+ size_t bus_id_start;
+ int r;
+
+ assert(dev);
+ assert(names);
+
+ /* Retrieve the associated CCW device */
+ r = sd_device_get_parent(dev, &cdev);
+ if (r < 0)
+ return r;
+
+ /* skip virtio subsystem if present */
+ cdev = skip_virtio(cdev);
+ if (!cdev)
+ return -ENOENT;
+
+ r = sd_device_get_subsystem(cdev, &subsys);
+ if (r < 0)
+ return r;
+
+ /* Network devices are either single or grouped CCW devices */
+ if (!STR_IN_SET(subsys, "ccwgroup", "ccw"))
+ return -ENOENT;
+
+ /* 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 r;
+
+ /* 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 -EINVAL;
+
+ /* 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;
+
+ /* Store the CCW bus-ID for use as network device name */
+ if (snprintf_ok(names->ccw_busid, sizeof(names->ccw_busid), "c%s", bus_id))
+ names->type = NET_CCW;
+
+ return 0;
+}
+
+static int names_mac(sd_device *dev, struct netnames *names) {
+ const char *s;
+ unsigned long i;
+ unsigned a1, a2, a3, a4, a5, a6;
+ int r;
+
+ /* Some kinds of devices tend to have hardware addresses
+ * that are impossible to use in an iface name.
+ */
+ r = sd_device_get_sysattr_value(dev, "type", &s);
+ if (r < 0)
+ return r;
+
+ i = strtoul(s, NULL, 0);
+ switch (i) {
+ /* The persistent part of a hardware address of an InfiniBand NIC
+ * is 8 bytes long. We cannot fit this much in an iface name.
+ */
+ case ARPHRD_INFINIBAND:
+ return -EINVAL;
+ default:
+ break;
+ }
+
+ /* check for NET_ADDR_PERM, skip random MAC addresses */
+ r = sd_device_get_sysattr_value(dev, "addr_assign_type", &s);
+ if (r < 0)
+ return r;
+ i = strtoul(s, NULL, 0);
+ if (i != 0)
+ return 0;
+
+ r = sd_device_get_sysattr_value(dev, "address", &s);
+ if (r < 0)
+ return r;
+ if (sscanf(s, "%x:%x:%x:%x:%x:%x", &a1, &a2, &a3, &a4, &a5, &a6) != 6)
+ return -EINVAL;
+
+ /* skip empty MAC addresses */
+ if (a1 + a2 + a3 + a4 + a5 + a6 == 0)
+ return -EINVAL;
+
+ names->mac[0] = a1;
+ names->mac[1] = a2;
+ names->mac[2] = a3;
+ names->mac[3] = a4;
+ names->mac[4] = a5;
+ names->mac[5] = a6;
+ names->mac_valid = true;
+ return 0;
+}
+
+static int names_netdevsim(sd_device *dev, struct netnames *names) {
+ sd_device *netdevsimdev;
+ const char *sysname;
+ unsigned addr;
+ const char *port_name = NULL;
+ int r;
+ bool ok;
+
+ if (!naming_scheme_has(NAMING_NETDEVSIM))
+ return 0;
+
+ assert(dev);
+ assert(names);
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "netdevsim", NULL, &netdevsimdev);
+ if (r < 0)
+ return r;
+ r = sd_device_get_sysname(netdevsimdev, &sysname);
+ if (r < 0)
+ return r;
+
+ if (sscanf(sysname, "netdevsim%u", &addr) != 1)
+ return -EINVAL;
+
+ r = sd_device_get_sysattr_value(dev, "phys_port_name", &port_name);
+ if (r < 0)
+ return r;
+
+ ok = snprintf_ok(names->netdevsim_path, sizeof(names->netdevsim_path), "i%un%s", addr, port_name);
+ if (!ok)
+ return -ENOBUFS;
+
+ names->type = NET_NETDEVSIM;
+
+ return 0;
+}
+
+/* IEEE Organizationally Unique Identifier vendor string */
+static int ieee_oui(sd_device *dev, struct netnames *names, bool test) {
+ char str[32];
+
+ if (!names->mac_valid)
+ return -ENOENT;
+ /* skip commonly misused 00:00:00 (Xerox) prefix */
+ if (memcmp(names->mac, "\0\0\0", 3) == 0)
+ return -EINVAL;
+ xsprintf(str, "OUI:%02X%02X%02X%02X%02X%02X", names->mac[0],
+ names->mac[1], names->mac[2], names->mac[3], names->mac[4],
+ names->mac[5]);
+ udev_builtin_hwdb_lookup(dev, NULL, str, NULL, test);
+ return 0;
+}
+
+static int builtin_net_id(sd_device *dev, int argc, char *argv[], bool test) {
+ const char *s, *p, *devtype, *prefix = "en";
+ struct netnames names = {};
+ unsigned long i;
+ int r;
+
+ /* handle only ARPHRD_ETHER, ARPHRD_SLIP and ARPHRD_INFINIBAND devices */
+ r = sd_device_get_sysattr_value(dev, "type", &s);
+ if (r < 0)
+ return r;
+
+ i = strtoul(s, NULL, 0);
+ switch (i) {
+ case ARPHRD_ETHER:
+ prefix = "en";
+ break;
+ case ARPHRD_INFINIBAND:
+ if (naming_scheme_has(NAMING_INFINIBAND))
+ prefix = "ib";
+ else
+ return 0;
+ break;
+ case ARPHRD_SLIP:
+ prefix = "sl";
+ break;
+ default:
+ return 0;
+ }
+
+ /* skip stacked devices, like VLANs, ... */
+ r = sd_device_get_sysattr_value(dev, "ifindex", &s);
+ if (r < 0)
+ return r;
+ r = sd_device_get_sysattr_value(dev, "iflink", &p);
+ if (r < 0)
+ return r;
+ if (!streq(s, p))
+ return 0;
+
+ if (sd_device_get_devtype(dev, &devtype) >= 0) {
+ if (streq("wlan", devtype))
+ prefix = "wl";
+ else if (streq("wwan", devtype))
+ prefix = "ww";
+ }
+
+ udev_builtin_add_property(dev, test, "ID_NET_NAMING_SCHEME", naming_scheme()->name);
+
+ r = names_mac(dev, &names);
+ if (r >= 0 && names.mac_valid) {
+ char str[ALTIFNAMSIZ];
+
+ xsprintf(str, "%sx%02x%02x%02x%02x%02x%02x", prefix,
+ names.mac[0], names.mac[1], names.mac[2],
+ names.mac[3], names.mac[4], names.mac[5]);
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_MAC", str);
+
+ ieee_oui(dev, &names, test);
+ }
+
+ /* get path names for Linux on System z network devices */
+ if (names_ccw(dev, &names) >= 0 && names.type == NET_CCW) {
+ char str[ALTIFNAMSIZ];
+
+ if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.ccw_busid))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+ return 0;
+ }
+
+ /* get ibmveth/ibmvnic slot-based names. */
+ if (names_vio(dev, &names) >= 0 && names.type == NET_VIO) {
+ char str[ALTIFNAMSIZ];
+
+ if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.vio_slot))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+ return 0;
+ }
+
+ /* get ACPI path names for ARM64 platform devices */
+ if (names_platform(dev, &names, test) >= 0 && names.type == NET_PLATFORM) {
+ char str[ALTIFNAMSIZ];
+
+ if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.platform_path))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+ return 0;
+ }
+
+ /* get netdevsim path names */
+ if (names_netdevsim(dev, &names) >= 0 && names.type == NET_NETDEVSIM) {
+ char str[ALTIFNAMSIZ];
+
+ if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.netdevsim_path))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+
+ return 0;
+ }
+
+ /* get PCI based path names, we compose only PCI based paths */
+ if (names_pci(dev, &names) < 0)
+ return 0;
+
+ /* plain PCI device */
+ if (names.type == NET_PCI) {
+ char str[ALTIFNAMSIZ];
+
+ if (names.pci_onboard[0] &&
+ snprintf_ok(str, sizeof str, "%s%s", prefix, names.pci_onboard))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_ONBOARD", str);
+
+ if (names.pci_onboard_label &&
+ snprintf_ok(str, sizeof str, "%s%s",
+ naming_scheme_has(NAMING_LABEL_NOPREFIX) ? "" : prefix,
+ names.pci_onboard_label))
+ udev_builtin_add_property(dev, test, "ID_NET_LABEL_ONBOARD", str);
+
+ if (names.pci_path[0] &&
+ snprintf_ok(str, sizeof str, "%s%s", prefix, names.pci_path))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+
+ if (names.pci_slot[0] &&
+ snprintf_ok(str, sizeof str, "%s%s", prefix, names.pci_slot))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+ return 0;
+ }
+
+ /* USB device */
+ if (names_usb(dev, &names) >= 0 && names.type == NET_USB) {
+ char str[ALTIFNAMSIZ];
+
+ if (names.pci_path[0] &&
+ snprintf_ok(str, sizeof str, "%s%s%s", prefix, names.pci_path, names.usb_ports))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+
+ if (names.pci_slot[0] &&
+ snprintf_ok(str, sizeof str, "%s%s%s", prefix, names.pci_slot, names.usb_ports))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+ return 0;
+ }
+
+ /* Broadcom bus */
+ if (names_bcma(dev, &names) >= 0 && names.type == NET_BCMA) {
+ char str[ALTIFNAMSIZ];
+
+ if (names.pci_path[0] &&
+ snprintf_ok(str, sizeof str, "%s%s%s", prefix, names.pci_path, names.bcma_core))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+
+ if (names.pci_slot[0] &&
+ snprintf(str, sizeof str, "%s%s%s", prefix, names.pci_slot, names.bcma_core))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+ return 0;
+ }
+
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_net_id = {
+ .name = "net_id",
+ .cmd = builtin_net_id,
+ .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..cb12b94
--- /dev/null
+++ b/src/udev/udev-builtin-net_setup_link.c
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "device-util.h"
+#include "alloc-util.h"
+#include "link-config.h"
+#include "log.h"
+#include "string-util.h"
+#include "udev-builtin.h"
+
+static link_config_ctx *ctx = NULL;
+
+static int builtin_net_setup_link(sd_device *dev, int argc, char **argv, bool test) {
+ _cleanup_free_ char *driver = NULL;
+ const char *name = NULL;
+ link_config *link;
+ int r;
+
+ if (argc > 1)
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ r = link_get_driver(ctx, dev, &driver);
+ if (r < 0)
+ log_device_full_errno(dev, r == -EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING,
+ r, "Failed to query device driver: %m");
+ else
+ udev_builtin_add_property(dev, test, "ID_NET_DRIVER", driver);
+
+ r = link_config_get(ctx, dev, &link);
+ if (r < 0) {
+ if (r == -ENOENT)
+ return log_device_debug_errno(dev, r, "No matching link configuration found.");
+
+ return log_device_error_errno(dev, r, "Failed to get link config: %m");
+ }
+
+ r = link_config_apply(ctx, link, dev, &name);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Could not apply link config, ignoring: %m");
+
+ udev_builtin_add_property(dev, test, "ID_NET_LINK_FILE", link->filename);
+
+ if (name)
+ udev_builtin_add_property(dev, test, "ID_NET_NAME", name);
+
+ 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) {
+ link_config_ctx_free(ctx);
+ ctx = NULL;
+ log_debug("Unloaded link configuration context.");
+}
+
+static bool builtin_net_setup_link_validate(void) {
+ log_debug("Check if link configuration needs reloading.");
+ if (!ctx)
+ return false;
+
+ return link_config_should_reload(ctx);
+}
+
+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,
+ .validate = builtin_net_setup_link_validate,
+ .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..0da59e2
--- /dev/null
+++ b/src/udev/udev-builtin-path_id.c
@@ -0,0 +1,732 @@
+/* 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 <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "libudev-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sysexits.h"
+#include "udev-builtin.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;
+
+ lun = strtoul(sysnum, NULL, 10);
+ 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);
+
+ 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;
+ struct dirent *dent;
+ 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(base, pos - base);
+ dir = opendir(base);
+ if (!dir)
+ return NULL;
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ char *rest;
+ int i;
+
+ if (dent->d_name[0] == '.')
+ continue;
+ if (!IN_SET(dent->d_type, DT_DIR, DT_LNK))
+ continue;
+ if (!startswith(dent->d_name, "host"))
+ continue;
+ i = strtoul(&dent->d_name[4], &rest, 10);
+ if (rest[0] != '\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 || i < basenum)
+ basenum = i;
+ }
+ if (basenum == -1)
+ return hostdev;
+ host -= basenum;
+
+ path_prepend(path, "scsi-%u:%u:%u:%u", 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];
+ size_t i, k;
+
+ 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;
+
+ for (i = 1, k = 0; 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 sd_device *handle_usb(sd_device *parent, char **path) {
+ const char *devtype, *str, *port;
+
+ 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++;
+
+ path_prepend(path, "usb-0:%s", port);
+ return skip_subsystem(parent, "usb");
+}
+
+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 builtin_path_id(sd_device *dev, int argc, char *argv[], bool test) {
+ sd_device *parent;
+ _cleanup_free_ char *path = NULL;
+ _cleanup_free_ char *compat_path = NULL;
+ bool supported_transport = false;
+ bool supported_parent = false;
+ const char *subsystem;
+
+ assert(dev);
+
+ /* walk up the chain of devices and compose path */
+ parent = dev;
+ while (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, "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 (streq(subsys, "nvme")) {
+ 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);
+ 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;
+
+ {
+ char tag[UTIL_NAME_SIZE];
+ size_t i;
+ const char *p;
+
+ /* compose valid udev tag name */
+ for (p = path, i = 0; *p; p++) {
+ if ((*p >= '0' && *p <= '9') ||
+ (*p >= 'A' && *p <= 'Z') ||
+ (*p >= 'a' && *p <= 'z') ||
+ *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';
+
+ udev_builtin_add_property(dev, test, "ID_PATH", path);
+ udev_builtin_add_property(dev, test, "ID_PATH_TAG", tag);
+ }
+
+ /*
+ * 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..cfdf130
--- /dev/null
+++ b/src/udev/udev-builtin-uaccess.c
@@ -0,0 +1,80 @@
+/* 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 "login-util.h"
+#include "logind-acl.h"
+#include "log.h"
+#include "udev-builtin.h"
+
+static int builtin_uaccess(sd_device *dev, int argc, char *argv[], bool test) {
+ 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");
+ if (r >= 0)
+ 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..fa554e7
--- /dev/null
+++ b/src/udev/udev-builtin-usb_id.c
@@ -0,0 +1,462 @@
+/* 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 <stdlib.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "libudev-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.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;
+ char *eptr;
+ const char *type = "generic";
+
+ type_num = strtoul(from, &eptr, 0);
+ if (eptr != from) {
+ 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) {
+ int type_num;
+ char *eptr;
+ const char *type = "generic";
+
+ type_num = strtoul(from, &eptr, 0);
+ if (eptr != from) {
+ 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 = -1;
+ 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);
+ 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(sd_device *dev, int argc, char *argv[], bool test) {
+ 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[UTIL_NAME_SIZE] = "";
+ char packed_if_str[UTIL_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;
+ int if_class_num;
+ int protocol = 0;
+ size_t l;
+ char *s;
+
+ const char *syspath, *sysname, *devtype, *interface_syspath;
+ int r;
+
+ assert(dev);
+
+ 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");
+
+ if_class_num = strtoul(if_class, NULL, 16);
+ 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:%d protocol:%d", 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;
+ }
+ udev_util_encode_string(scsi_vendor, vendor_str_enc, sizeof(vendor_str_enc));
+ util_replace_whitespace(scsi_vendor, vendor_str, sizeof(vendor_str)-1);
+ util_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;
+ }
+ udev_util_encode_string(scsi_model, model_str_enc, sizeof(model_str_enc));
+ util_replace_whitespace(scsi_model, model_str, sizeof(model_str)-1);
+ util_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;
+ }
+ util_replace_whitespace(scsi_rev, revision_str, sizeof(revision_str)-1);
+ util_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;
+ udev_util_encode_string(usb_vendor, vendor_str_enc, sizeof(vendor_str_enc));
+ util_replace_whitespace(usb_vendor, vendor_str, sizeof(vendor_str)-1);
+ util_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;
+ udev_util_encode_string(usb_model, model_str_enc, sizeof(model_str_enc));
+ util_replace_whitespace(usb_model, model_str, sizeof(model_str)-1);
+ util_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) {
+ util_replace_whitespace(usb_rev, revision_str, sizeof(revision_str)-1);
+ util_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) {
+ const unsigned char *p;
+
+ /* http://msdn.microsoft.com/en-us/library/windows/hardware/gg487321.aspx */
+ for (p = (unsigned char *) usb_serial; *p != '\0'; p++)
+ if (*p < 0x20 || *p > 0x7f || *p == ',') {
+ usb_serial = NULL;
+ break;
+ }
+
+ if (usb_serial) {
+ util_replace_whitespace(usb_serial, serial_str, sizeof(serial_str)-1);
+ util_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);
+
+ 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_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_REVISION", revision_str);
+ udev_builtin_add_property(dev, test, "ID_SERIAL", serial);
+ if (!isempty(serial_str))
+ udev_builtin_add_property(dev, test, "ID_SERIAL_SHORT", serial_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);
+ udev_builtin_add_property(dev, test, "ID_BUS", "usb");
+ 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..80d1766
--- /dev/null
+++ b/src/udev/udev-builtin.c
@@ -0,0 +1,145 @@
+/* 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_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) {
+ unsigned i;
+
+ if (initialized)
+ return;
+
+ for (i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && builtins[i]->init)
+ builtins[i]->init();
+
+ initialized = true;
+}
+
+void udev_builtin_exit(void) {
+ unsigned i;
+
+ if (!initialized)
+ return;
+
+ for (i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && builtins[i]->exit)
+ builtins[i]->exit();
+
+ initialized = false;
+}
+
+bool udev_builtin_validate(void) {
+ unsigned i;
+
+ for (i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && builtins[i]->validate && builtins[i]->validate())
+ return true;
+ return false;
+}
+
+void udev_builtin_list(void) {
+ unsigned i;
+
+ for (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) {
+ UdevBuiltinCommand i;
+ size_t n;
+
+ assert(command);
+
+ command += strspn(command, WHITESPACE);
+ n = strcspn(command, WHITESPACE);
+ for (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(sd_device *dev, UdevBuiltinCommand cmd, const char *command, bool test) {
+ _cleanup_strv_free_ char **argv = NULL;
+ int r;
+
+ assert(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(dev, 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;
+}
diff --git a/src/udev/udev-builtin.h b/src/udev/udev-builtin.h
new file mode 100644
index 0000000..14d6406
--- /dev/null
+++ b/src/udev/udev-builtin.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-device.h"
+
+typedef enum {
+#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_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 = -1,
+} UdevBuiltinCommand;
+
+typedef struct UdevBuiltin {
+ const char *name;
+ int (*cmd)(sd_device *dev, int argc, char *argv[], bool test);
+ const char *help;
+ int (*init)(void);
+ void (*exit)(void);
+ bool (*validate)(void);
+ bool run_once;
+} UdevBuiltin;
+
+#define PTR_TO_UDEV_BUILTIN_CMD(p) ((UdevBuiltinCommand) ((intptr_t) (p)-1))
+#define UDEV_BUILTIN_CMD_TO_PTR(u) ((void *) ((intptr_t) (u)+1))
+
+#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_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(sd_device *dev, UdevBuiltinCommand cmd, const char *command, bool test);
+void udev_builtin_list(void);
+bool udev_builtin_validate(void);
+int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val);
+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..48355aa
--- /dev/null
+++ b/src/udev/udev-ctrl.c
@@ -0,0 +1,393 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * libudev - interface to udev device information
+ *
+ * This library 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.
+ */
+
+#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 "io-util.h"
+#include "socket-util.h"
+#include "strxcpyx.h"
+#include "udev-ctrl.h"
+#include "util.h"
+
+/* wire protocol magic must match */
+#define UDEV_CTRL_MAGIC 0xdead1dea
+
+struct udev_ctrl_msg_wire {
+ char version[16];
+ unsigned magic;
+ enum udev_ctrl_msg_type type;
+ union udev_ctrl_msg_value value;
+};
+
+struct udev_ctrl {
+ unsigned n_ref;
+ int sock;
+ int sock_connect;
+ union sockaddr_union saddr;
+ socklen_t addrlen;
+ bool bound:1;
+ bool cleanup_socket:1;
+ bool connected:1;
+ bool maybe_disconnected:1;
+ 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(struct udev_ctrl **ret, int fd) {
+ _cleanup_close_ int sock = -1;
+ struct udev_ctrl *uctrl;
+
+ assert(ret);
+
+ if (fd < 0) {
+ sock = socket(AF_LOCAL, SOCK_SEQPACKET|SOCK_NONBLOCK|SOCK_CLOEXEC, 0);
+ if (sock < 0)
+ return log_error_errno(errno, "Failed to create socket: %m");
+ }
+
+ uctrl = new(struct udev_ctrl, 1);
+ if (!uctrl)
+ return -ENOMEM;
+
+ *uctrl = (struct udev_ctrl) {
+ .n_ref = 1,
+ .sock = fd >= 0 ? fd : TAKE_FD(sock),
+ .sock_connect = -1,
+ .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(struct udev_ctrl *uctrl) {
+ int r;
+
+ assert(uctrl);
+
+ if (uctrl->bound)
+ return 0;
+
+ r = bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen);
+ if (r < 0 && errno == EADDRINUSE) {
+ (void) sockaddr_un_unlink(&uctrl->saddr.un);
+ r = bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen);
+ }
+
+ if (r < 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;
+ uctrl->cleanup_socket = true;
+
+ return 0;
+}
+
+static void udev_ctrl_disconnect(struct udev_ctrl *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 struct udev_ctrl *udev_ctrl_free(struct udev_ctrl *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(struct udev_ctrl, udev_ctrl, udev_ctrl_free);
+
+int udev_ctrl_cleanup(struct udev_ctrl *uctrl) {
+ if (!uctrl)
+ return 0;
+ if (uctrl->cleanup_socket)
+ sockaddr_un_unlink(&uctrl->saddr.un);
+ return 0;
+}
+
+int udev_ctrl_attach_event(struct udev_ctrl *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(struct udev_ctrl *uctrl) {
+ assert(uctrl);
+
+ return uctrl->event_source;
+}
+
+static void udev_ctrl_disconnect_and_listen_again(struct udev_ctrl *uctrl) {
+ udev_ctrl_disconnect(uctrl);
+ udev_ctrl_unref(uctrl);
+ (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_ON);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl *, udev_ctrl_disconnect_and_listen_again);
+
+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) struct udev_ctrl *uctrl = NULL;
+ struct udev_ctrl_msg_wire msg_wire;
+ struct iovec iov = IOVEC_MAKE(&msg_wire, sizeof(struct udev_ctrl_msg_wire));
+ 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 cmsghdr *cmsg;
+ 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);
+
+ cmsg = CMSG_FIRSTHDR(&smsg);
+
+ if (!cmsg || cmsg->cmsg_type != SCM_CREDENTIALS) {
+ log_error("No sender credentials received, ignoring message");
+ return 0;
+ }
+
+ cred = (struct ucred *) CMSG_DATA(cmsg);
+
+ 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) {
+ struct udev_ctrl *uctrl = userdata;
+ _cleanup_close_ int sock = -1;
+ struct ucred ucred;
+ int r;
+
+ assert(uctrl);
+
+ 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(struct udev_ctrl *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(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, int intval, const char *buf) {
+ struct udev_ctrl_msg_wire ctrl_msg_wire = {
+ .version = "udev-" STRINGIFY(PROJECT_VERSION),
+ .magic = UDEV_CTRL_MAGIC,
+ .type = type,
+ };
+
+ if (uctrl->maybe_disconnected)
+ return -ENOANO; /* to distinguish this from other errors. */
+
+ if (buf)
+ strscpy(ctrl_msg_wire.value.buf, sizeof(ctrl_msg_wire.value.buf), buf);
+ else
+ ctrl_msg_wire.value.intval = intval;
+
+ 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;
+
+ if (type == UDEV_CTRL_EXIT)
+ uctrl->maybe_disconnected = true;
+
+ return 0;
+}
+
+int udev_ctrl_wait(struct udev_ctrl *uctrl, usec_t timeout) {
+ _cleanup_(sd_event_source_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;
+
+ if (!uctrl->maybe_disconnected) {
+ r = udev_ctrl_send(uctrl, _UDEV_CTRL_END_MESSAGES, 0, 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_or_monotonic(),
+ 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..680fbf7
--- /dev/null
+++ b/src/udev/udev-ctrl.h
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "sd-event.h"
+
+#include "macro.h"
+#include "time-util.h"
+
+struct udev_ctrl;
+
+enum udev_ctrl_msg_type {
+ _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,
+};
+
+union udev_ctrl_msg_value {
+ int intval;
+ char buf[256];
+};
+
+typedef int (*udev_ctrl_handler_t)(struct udev_ctrl *udev_ctrl, enum udev_ctrl_msg_type type,
+ const union udev_ctrl_msg_value *value, void *userdata);
+
+int udev_ctrl_new_from_fd(struct udev_ctrl **ret, int fd);
+static inline int udev_ctrl_new(struct udev_ctrl **ret) {
+ return udev_ctrl_new_from_fd(ret, -1);
+}
+
+int udev_ctrl_enable_receiving(struct udev_ctrl *uctrl);
+struct udev_ctrl *udev_ctrl_ref(struct udev_ctrl *uctrl);
+struct udev_ctrl *udev_ctrl_unref(struct udev_ctrl *uctrl);
+int udev_ctrl_cleanup(struct udev_ctrl *uctrl);
+int udev_ctrl_attach_event(struct udev_ctrl *uctrl, sd_event *event);
+int udev_ctrl_start(struct udev_ctrl *uctrl, udev_ctrl_handler_t callback, void *userdata);
+sd_event_source *udev_ctrl_get_event_source(struct udev_ctrl *uctrl);
+
+int udev_ctrl_wait(struct udev_ctrl *uctrl, usec_t timeout);
+
+int udev_ctrl_send(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, int intval, const char *buf);
+static inline int udev_ctrl_send_set_log_level(struct udev_ctrl *uctrl, int priority) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_SET_LOG_LEVEL, priority, NULL);
+}
+
+static inline int udev_ctrl_send_stop_exec_queue(struct udev_ctrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_STOP_EXEC_QUEUE, 0, NULL);
+}
+
+static inline int udev_ctrl_send_start_exec_queue(struct udev_ctrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_START_EXEC_QUEUE, 0, NULL);
+}
+
+static inline int udev_ctrl_send_reload(struct udev_ctrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_RELOAD, 0, NULL);
+}
+
+static inline int udev_ctrl_send_set_env(struct udev_ctrl *uctrl, const char *key) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_SET_ENV, 0, key);
+}
+
+static inline int udev_ctrl_send_set_children_max(struct udev_ctrl *uctrl, int count) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_SET_CHILDREN_MAX, count, NULL);
+}
+
+static inline int udev_ctrl_send_ping(struct udev_ctrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_PING, 0, NULL);
+}
+
+static inline int udev_ctrl_send_exit(struct udev_ctrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_EXIT, 0, NULL);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl*, udev_ctrl_unref);
diff --git a/src/udev/udev-event.c b/src/udev/udev-event.c
new file mode 100644
index 0000000..5159d19
--- /dev/null
+++ b/src/udev/udev-event.c
@@ -0,0 +1,1087 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "format-util.h"
+#include "libudev-util.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "signal-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+#include "udev-event.h"
+#include "udev-node.h"
+#include "udev-util.h"
+#include "udev-watch.h"
+#include "user-util.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;
+} Spawn;
+
+UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl) {
+ 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,
+ };
+
+ 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);
+
+ return mfree(event);
+}
+
+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 = -1
+} 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 UTIL_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 >= UTIL_PATH_SIZE)
+ return -EINVAL;
+
+ strnscpy(ret_attr, UTIL_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(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) {
+ sd_device *parent, *dev = event->dev;
+ const char *val = NULL;
+ char *s = dest;
+ int r;
+
+ switch (type) {
+ case FORMAT_SUBST_DEVPATH:
+ r = sd_device_get_devpath(dev, &val);
+ if (r < 0)
+ return r;
+ l = strpcpy(&s, l, val);
+ break;
+ case FORMAT_SUBST_KERNEL:
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return r;
+ l = strpcpy(&s, l, val);
+ break;
+ case FORMAT_SUBST_KERNEL_NUMBER:
+ r = sd_device_get_sysnum(dev, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ l = strpcpy(&s, l, val);
+ 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;
+ l = strpcpy(&s, l, val);
+ 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;
+ l = strpcpy(&s, l, val);
+ 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;
+ l = strpcpyf(&s, l, "%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)
+ l = strpcpy(&s, l, event->program_result);
+ 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)
+ l = strpcpy(&s, l, start);
+ else {
+ while (*p && !strchr(WHITESPACE, *p))
+ p++;
+ l = strnpcpy(&s, l, start, p - start);
+ }
+ }
+ break;
+ }
+ case FORMAT_SUBST_ATTR: {
+ char vbuf[UTIL_NAME_SIZE];
+ int count;
+
+ if (isempty(attr))
+ return -EINVAL;
+
+ /* try to read the value specified by "[dmi/id]product_name" */
+ if (util_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(vbuf, sizeof(vbuf), val);
+ delete_trailing_chars(vbuf, NULL);
+ count = util_replace_chars(vbuf, UDEV_ALLOWED_CHARS_INPUT);
+ if (count > 0)
+ log_device_debug(dev, "%i character(s) replaced", count);
+ l = strpcpy(&s, l, vbuf);
+ 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;
+ l = strpcpy(&s, l, val + STRLEN("/dev/"));
+ break;
+ case FORMAT_SUBST_DEVNODE:
+ r = sd_device_get_devname(dev, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ l = strpcpy(&s, l, val);
+ break;
+ case FORMAT_SUBST_NAME:
+ if (event->name)
+ l = strpcpy(&s, l, event->name);
+ else if (sd_device_get_devname(dev, &val) >= 0)
+ l = strpcpy(&s, l, val + STRLEN("/dev/"));
+ else {
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return r;
+ l = strpcpy(&s, l, val);
+ }
+ break;
+ case FORMAT_SUBST_LINKS:
+ FOREACH_DEVICE_DEVLINK(dev, val)
+ if (s == dest)
+ l = strpcpy(&s, l, val + STRLEN("/dev/"));
+ else
+ l = strpcpyl(&s, l, " ", val + STRLEN("/dev/"), NULL);
+ if (s == dest)
+ goto null_terminate;
+ break;
+ case FORMAT_SUBST_ROOT:
+ l = strpcpy(&s, l, "/dev");
+ break;
+ case FORMAT_SUBST_SYS:
+ l = strpcpy(&s, l, "/sys");
+ break;
+ case FORMAT_SUBST_ENV:
+ if (isempty(attr))
+ return -EINVAL;
+ r = sd_device_get_property_value(dev, attr, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ l = strpcpy(&s, l, val);
+ break;
+ default:
+ assert_not_reached("Unknown format substitution type");
+ }
+
+ return s - dest;
+
+null_terminate:
+ *s = '\0';
+ return 0;
+}
+
+size_t udev_event_apply_format(UdevEvent *event,
+ const char *src, char *dest, size_t size,
+ bool replace_whitespace) {
+ const char *s = src;
+ int r;
+
+ assert(event);
+ assert(event->dev);
+ assert(src);
+ assert(dest);
+ assert(size > 0);
+
+ while (*s) {
+ FormatSubstitutionType type;
+ char attr[UTIL_PATH_SIZE];
+ ssize_t subst_len;
+
+ 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 */
+ break;
+ *dest++ = *s++;
+ size--;
+ continue;
+ }
+
+ subst_len = udev_event_subst_format(event, type, attr, dest, size);
+ 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;
+ }
+
+ /* FORMAT_SUBST_RESULT handles spaces itself */
+ if (replace_whitespace && type != FORMAT_SUBST_RESULT)
+ /* util_replace_whitespace can replace in-place,
+ * and does nothing if subst_len == 0 */
+ subst_len = util_replace_whitespace(dest, dest, subst_len);
+
+ dest += subst_len;
+ size -= subst_len;
+ }
+
+ assert(size >= 1);
+ *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[UTIL_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;
+}
+
+static int on_spawn_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Spawn *spawn = userdata;
+ char buf[4096], *p;
+ size_t size;
+ ssize_t l;
+ int r;
+
+ assert(spawn);
+ 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 - 1);
+ 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;
+ }
+
+ 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;
+ char **q;
+
+ v = strv_split_newlines(p);
+ if (!v)
+ return 0;
+
+ STRV_FOREACH(q, v)
+ log_device_debug(spawn->device, "'%s'(%s) '%s'", spawn->cmd,
+ fd == spawn->fd_stdout ? "out" : "err", *q);
+ }
+
+
+ if (l == 0)
+ return 0;
+
+ /* Re-enable the event source if we did not encounter EOF */
+reenable:
+ 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 = userdata;
+ char timeout[FORMAT_TIMESPAN_MAX];
+
+ assert(spawn);
+
+ 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(timeout, sizeof(timeout), 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 = userdata;
+ char timeout[FORMAT_TIMESPAN_MAX];
+
+ assert(spawn);
+
+ log_device_warning(spawn->device, "Spawned process '%s' ["PID_FMT"] is taking longer than %s to complete",
+ spawn->cmd, spawn->pid,
+ format_timespan(timeout, sizeof(timeout), 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 = userdata;
+ int ret = -EIO;
+
+ assert(spawn);
+
+ 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);
+ }
+
+ 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_unrefp) sd_event_source *sigchld_source = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *stdout_source = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *stderr_source = NULL;
+ int r;
+
+ assert(spawn);
+
+ r = sd_event_new(&e);
+ if (r < 0)
+ return r;
+
+ 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 r;
+ }
+
+ 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 r;
+ }
+ }
+
+ 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 r;
+ r = sd_event_source_set_enabled(stdout_source, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+ }
+
+ 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 r;
+ r = sd_event_source_set_enabled(stderr_source, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_event_add_child(e, &sigchld_source, spawn->pid, WEXITED, on_spawn_sigchld, spawn);
+ if (r < 0)
+ return r;
+ /* 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 r;
+
+
+ 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 ressize) {
+ _cleanup_close_pair_ int outpipe[2] = {-1, -1}, errpipe[2] = {-1, -1};
+ _cleanup_strv_free_ char **argv = NULL;
+ char **envp = NULL;
+ Spawn spawn;
+ pid_t pid;
+ int r;
+
+ assert(event);
+ assert(event->dev);
+ assert(result || ressize == 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("(spawn)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return log_device_error_errno(event->dev, r,
+ "Failed to fork() to execute command '%s': %m", cmd);
+ if (r == 0) {
+ if (rearrange_stdio(-1, outpipe[WRITE_END], errpipe[WRITE_END]) < 0)
+ _exit(EXIT_FAILURE);
+
+ (void) close_all_fds(NULL, 0);
+ (void) rlimit_nofile_safe();
+
+ 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 = ressize,
+ };
+ 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';
+
+ return r; /* 0 for success, and positive if the program failed */
+}
+
+static int rename_netif(UdevEvent *event) {
+ sd_device *dev = event->dev;
+ const char *oldname;
+ int ifindex, r;
+
+ if (!event->name)
+ return 0; /* No new name is requested. */
+
+ r = sd_device_get_sysname(dev, &oldname);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get sysname: %m");
+
+ if (streq(event->name, oldname))
+ return 0; /* The interface name is already requested name. */
+
+ if (!device_for_action(dev, DEVICE_ACTION_ADD))
+ return 0; /* Rename the interface only when it is added. */
+
+ r = sd_device_get_ifindex(dev, &ifindex);
+ if (r == -ENOENT)
+ return 0; /* Device is not a network interface. */
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get ifindex: %m");
+
+ /* Set ID_RENAMING boolean property here, and drop it in the corresponding move uevent later. */
+ r = device_add_property(dev, "ID_RENAMING", "1");
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to add 'ID_RENAMING' property: %m");
+
+ r = device_rename(dev, event->name);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to update properties with new name '%s': %m", event->name);
+
+ /* 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)
+ return log_device_warning_errno(event->dev_db_clone, r, "Failed to add 'ID_RENAMING' property: %m");
+
+ r = device_update_db(event->dev_db_clone);
+ if (r < 0)
+ return log_device_debug_errno(event->dev_db_clone, r, "Failed to update database under /run/udev/data/: %m");
+
+ r = rtnl_set_link_name(&event->rtnl, ifindex, event->name);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to rename network interface %i from '%s' to '%s': %m",
+ ifindex, oldname, event->name);
+
+ log_device_debug(dev, "Network interface %i is renamed from '%s' to '%s'", ifindex, oldname, event->name);
+
+ return 1;
+}
+
+static int update_devnode(UdevEvent *event) {
+ sd_device *dev = 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");
+
+ /* remove/update possible left-over symlinks from old database entry */
+ (void) udev_node_update_old_links(dev, event->dev_db_clone);
+
+ 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");
+ }
+ if (event->mode == MODE_INVALID && gid_is_valid(event->gid) && event->gid > 0)
+ /* If group is set, but mode is not set, "upgrade" mode for the group. */
+ event->mode = 0660;
+
+ bool apply_mac = device_for_action(dev, DEVICE_ACTION_ADD);
+
+ return udev_node_add(dev, apply_mac, event->mode, event->uid, event->gid, event->seclabel_list);
+}
+
+static void event_execute_rules_on_remove(
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRules *rules) {
+
+ sd_device *dev = 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");
+
+ if (sd_device_get_devnum(dev, NULL) >= 0)
+ (void) udev_watch_end(dev);
+
+ (void) 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);
+}
+
+static int udev_event_on_move(sd_device *dev) {
+ int r;
+
+ /* Drop previously added property */
+ r = device_add_property(dev, "ID_RENAMING", NULL);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to remove 'ID_RENAMING' property: %m");
+
+ return 0;
+}
+
+static int copy_all_tags(sd_device *d, sd_device *s) {
+ const char *tag;
+ 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,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRules *rules) {
+ const char *subsystem;
+ DeviceAction action;
+ sd_device *dev;
+ int r;
+
+ assert(event);
+ assert(rules);
+
+ dev = event->dev;
+
+ r = sd_device_get_subsystem(dev, &subsystem);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get subsystem: %m");
+
+ r = device_get_action(dev, &action);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get ACTION: %m");
+
+ if (action == DEVICE_ACTION_REMOVE) {
+ event_execute_rules_on_remove(event, timeout_usec, timeout_signal, properties_list, rules);
+ return 0;
+ }
+
+ 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");
+
+ if (sd_device_get_devnum(dev, NULL) >= 0)
+ /* Disable watch during event processing. */
+ (void) udev_watch_end(event->dev_db_clone);
+
+ if (action == DEVICE_ACTION_MOVE) {
+ r = udev_event_on_move(event->dev);
+ if (r < 0)
+ return r;
+ }
+
+ 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");
+
+ r = rename_netif(event);
+ if (r < 0)
+ return r;
+
+ 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");
+
+ /* Yes, we run update_devnode() twice, because in the first invocation, that is before update of udev database,
+ * it could happen that two contenders are replacing each other's symlink. Hence we run it again to make sure
+ * symlinks point to devices that claim them with the highest priority. */
+ r = update_devnode(event);
+ if (r < 0)
+ return r;
+
+ device_set_is_initialized(dev);
+
+ return 0;
+}
+
+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->dev, 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) {
+ char buf[FORMAT_TIMESPAN_MAX];
+
+ log_device_debug(event->dev, "Delaying execution of \"%s\" for %s.",
+ command, format_timespan(buf, sizeof(buf), event->exec_delay_usec, USEC_PER_SEC));
+ (void) usleep(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);
+ 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-event.h b/src/udev/udev-event.h
new file mode 100644
index 0000000..8647828
--- /dev/null
+++ b/src/udev/udev-event.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+/*
+ * Copyright © 2003 Greg Kroah-Hartman <greg@kroah.com>
+ */
+
+#include "sd-device.h"
+#include "sd-netlink.h"
+
+#include "hashmap.h"
+#include "macro.h"
+#include "udev-rules.h"
+#include "udev-util.h"
+#include "util.h"
+
+#define READ_END 0
+#define WRITE_END 1
+
+typedef struct UdevEvent {
+ sd_device *dev;
+ sd_device *dev_parent;
+ sd_device *dev_db_clone;
+ char *name;
+ 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:1;
+ bool inotify_watch_final:1;
+ bool group_final:1;
+ bool owner_final:1;
+ bool mode_final:1;
+ bool name_final:1;
+ bool devlink_final:1;
+ bool run_final:1;
+} UdevEvent;
+
+UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl);
+UdevEvent *udev_event_free(UdevEvent *event);
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevEvent*, udev_event_free);
+
+size_t udev_event_apply_format(UdevEvent *event,
+ const char *src, char *dest, size_t size,
+ bool replace_whitespace);
+int udev_check_format(const char *value, size_t *offset, const char **hint);
+int udev_event_spawn(UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ bool accept_failure,
+ const char *cmd, char *result, size_t ressize);
+int udev_event_execute_rules(UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRules *rules);
+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-node.c b/src/udev/udev-node.c
new file mode 100644
index 0000000..b8b93ee
--- /dev/null
+++ b/src/udev/udev-node.c
@@ -0,0 +1,528 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "device-nodes.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "libudev-util.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "selinux-util.h"
+#include "smack-util.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-node.h"
+#include "user-util.h"
+
+#define LINK_UPDATE_MAX_RETRIES 128
+
+static int node_symlink(sd_device *dev, const char *node, const char *slink) {
+ _cleanup_free_ char *slink_dirname = NULL, *target = NULL;
+ const char *id_filename, *slink_tmp;
+ struct stat stats;
+ int r;
+
+ assert(dev);
+ assert(node);
+ assert(slink);
+
+ slink_dirname = dirname_malloc(slink);
+ if (!slink_dirname)
+ return log_oom();
+
+ /* use relative link */
+ r = path_make_relative(slink_dirname, node, &target);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get relative path from '%s' to '%s': %m", slink, node);
+
+ /* preserve link with correct target, do not replace node of other device */
+ if (lstat(slink, &stats) == 0) {
+ if (S_ISBLK(stats.st_mode) || S_ISCHR(stats.st_mode))
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Conflicting device node '%s' found, link to '%s' will not be created.", slink, node);
+ else if (S_ISLNK(stats.st_mode)) {
+ _cleanup_free_ char *buf = NULL;
+
+ if (readlink_malloc(slink, &buf) >= 0 &&
+ streq(target, buf)) {
+ log_device_debug(dev, "Preserve already existing symlink '%s' to '%s'", slink, target);
+ (void) label_fix(slink, LABEL_IGNORE_ENOENT);
+ (void) utimensat(AT_FDCWD, slink, NULL, AT_SYMLINK_NOFOLLOW);
+ return 0;
+ }
+ }
+ } else {
+ log_device_debug(dev, "Creating symlink '%s' to '%s'", slink, target);
+ do {
+ r = mkdir_parents_label(slink, 0755);
+ if (!IN_SET(r, 0, -ENOENT))
+ break;
+ mac_selinux_create_file_prepare(slink, S_IFLNK);
+ if (symlink(target, slink) < 0)
+ r = -errno;
+ mac_selinux_create_file_clear();
+ } while (r == -ENOENT);
+ if (r == 0)
+ return 0;
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to create symlink '%s' to '%s', trying to replace '%s': %m", slink, target, slink);
+ }
+
+ log_device_debug(dev, "Atomically replace '%s'", slink);
+ r = device_get_id_filename(dev, &id_filename);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get id_filename: %m");
+ slink_tmp = strjoina(slink, ".tmp-", id_filename);
+ (void) unlink(slink_tmp);
+ do {
+ r = mkdir_parents_label(slink_tmp, 0755);
+ if (!IN_SET(r, 0, -ENOENT))
+ break;
+ mac_selinux_create_file_prepare(slink_tmp, S_IFLNK);
+ if (symlink(target, slink_tmp) < 0)
+ r = -errno;
+ mac_selinux_create_file_clear();
+ } while (r == -ENOENT);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to create symlink '%s' to '%s': %m", slink_tmp, target);
+
+ if (rename(slink_tmp, slink) < 0) {
+ r = log_device_error_errno(dev, errno, "Failed to rename '%s' to '%s': %m", slink_tmp, slink);
+ (void) unlink(slink_tmp);
+ } else
+ /* Tell caller that we replaced already existing symlink. */
+ r = 1;
+
+ return r;
+}
+
+/* find device node of device with highest priority */
+static int link_find_prioritized(sd_device *dev, bool add, const char *stackdir, char **ret) {
+ _cleanup_closedir_ DIR *dir = NULL;
+ _cleanup_free_ char *target = NULL;
+ struct dirent *dent;
+ int r, priority = 0;
+
+ assert(!add || dev);
+ assert(stackdir);
+ assert(ret);
+
+ if (add) {
+ const char *devnode;
+
+ r = device_get_devlink_priority(dev, &priority);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return r;
+
+ target = strdup(devnode);
+ if (!target)
+ return -ENOMEM;
+ }
+
+ dir = opendir(stackdir);
+ if (!dir) {
+ if (target) {
+ *ret = TAKE_PTR(target);
+ return 0;
+ }
+
+ return -errno;
+ }
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ _cleanup_(sd_device_unrefp) sd_device *dev_db = NULL;
+ const char *devnode, *id_filename;
+ int db_prio = 0;
+
+ if (dent->d_name[0] == '\0')
+ break;
+ if (dent->d_name[0] == '.')
+ continue;
+
+ log_device_debug(dev, "Found '%s' claiming '%s'", dent->d_name, stackdir);
+
+ if (device_get_id_filename(dev, &id_filename) < 0)
+ continue;
+
+ /* did we find ourself? */
+ if (streq(dent->d_name, id_filename))
+ continue;
+
+ if (sd_device_new_from_device_id(&dev_db, dent->d_name) < 0)
+ continue;
+
+ if (sd_device_get_devname(dev_db, &devnode) < 0)
+ continue;
+
+ if (device_get_devlink_priority(dev_db, &db_prio) < 0)
+ continue;
+
+ if (target && db_prio <= priority)
+ continue;
+
+ log_device_debug(dev_db, "Device claims priority %i for '%s'", db_prio, stackdir);
+
+ r = free_and_strdup(&target, devnode);
+ if (r < 0)
+ return r;
+ priority = db_prio;
+ }
+
+ if (!target)
+ return -ENOENT;
+
+ *ret = TAKE_PTR(target);
+ return 0;
+}
+
+/* manage "stack of names" with possibly specified device priorities */
+static int link_update(sd_device *dev, const char *slink, bool add) {
+ _cleanup_free_ char *filename = NULL, *dirname = NULL;
+ char name_enc[PATH_MAX];
+ const char *id_filename;
+ int i, r, retries;
+
+ assert(dev);
+ assert(slink);
+
+ r = device_get_id_filename(dev, &id_filename);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get id_filename: %m");
+
+ util_path_encode(slink + STRLEN("/dev"), name_enc, sizeof(name_enc));
+ dirname = path_join("/run/udev/links/", name_enc);
+ if (!dirname)
+ return log_oom();
+ filename = path_join(dirname, id_filename);
+ if (!filename)
+ return log_oom();
+
+ if (!add) {
+ if (unlink(filename) == 0)
+ (void) rmdir(dirname);
+ } else
+ for (;;) {
+ _cleanup_close_ int fd = -1;
+
+ r = mkdir_parents(filename, 0755);
+ if (!IN_SET(r, 0, -ENOENT))
+ return r;
+
+ fd = open(filename, O_WRONLY|O_CREAT|O_CLOEXEC|O_TRUNC|O_NOFOLLOW, 0444);
+ if (fd >= 0)
+ break;
+ if (errno != ENOENT)
+ return -errno;
+ }
+
+ /* If the database entry is not written yet we will just do one iteration and possibly wrong symlink
+ * will be fixed in the second invocation. */
+ retries = sd_device_get_is_initialized(dev) > 0 ? LINK_UPDATE_MAX_RETRIES : 1;
+
+ for (i = 0; i < retries; i++) {
+ _cleanup_free_ char *target = NULL;
+ struct stat st1 = {}, st2 = {};
+
+ r = stat(dirname, &st1);
+ if (r < 0 && errno != ENOENT)
+ return -errno;
+
+ r = link_find_prioritized(dev, add, dirname, &target);
+ if (r == -ENOENT) {
+ log_device_debug(dev, "No reference left, removing '%s'", slink);
+ if (unlink(slink) == 0)
+ (void) rmdir_parents(slink, "/");
+
+ break;
+ } else if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to determine highest priority symlink: %m");
+
+ r = node_symlink(dev, target, slink);
+ if (r < 0) {
+ (void) unlink(filename);
+ break;
+ } else if (r == 1)
+ /* We have replaced already existing symlink, possibly there is some other device trying
+ * to claim the same symlink. Let's do one more iteration to give us a chance to fix
+ * the error if other device actually claims the symlink with higher priority. */
+ continue;
+
+ /* Skip the second stat() if the first failed, stat_inode_unmodified() would return false regardless. */
+ if ((st1.st_mode & S_IFMT) != 0) {
+ r = stat(dirname, &st2);
+ if (r < 0 && errno != ENOENT)
+ return -errno;
+
+ if (stat_inode_unmodified(&st1, &st2))
+ break;
+ }
+ }
+
+ return i < LINK_UPDATE_MAX_RETRIES ? 0 : -ELOOP;
+}
+
+int udev_node_update_old_links(sd_device *dev, sd_device *dev_old) {
+ const char *name, *devpath;
+ int r;
+
+ assert(dev);
+ assert(dev_old);
+
+ r = sd_device_get_devpath(dev, &devpath);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devpath: %m");
+
+ /* update possible left-over symlinks */
+ FOREACH_DEVICE_DEVLINK(dev_old, name) {
+ const char *name_current;
+ bool found = false;
+
+ /* check if old link name still belongs to this device */
+ FOREACH_DEVICE_DEVLINK(dev, name_current)
+ if (streq(name, name_current)) {
+ found = true;
+ break;
+ }
+
+ if (found)
+ continue;
+
+ log_device_debug(dev, "Updating old name, '%s' no longer belonging to '%s'",
+ name, devpath);
+ link_update(dev, name, false);
+ }
+
+ return 0;
+}
+
+static int node_permissions_apply(sd_device *dev, bool apply_mac,
+ mode_t mode, uid_t uid, gid_t gid,
+ OrderedHashmap *seclabel_list) {
+ const char *devnode, *subsystem, *id_filename = NULL;
+ bool apply_mode, apply_uid, apply_gid;
+ _cleanup_close_ int node_fd = -1;
+ struct stat stats;
+ dev_t devnum;
+ 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");
+ r = sd_device_get_subsystem(dev, &subsystem);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get subsystem: %m");
+ r = sd_device_get_devnum(dev, &devnum);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devnum: %m");
+ (void) device_get_id_filename(dev, &id_filename);
+
+ if (streq(subsystem, "block"))
+ mode |= S_IFBLK;
+ else
+ mode |= S_IFCHR;
+
+ node_fd = open(devnode, O_PATH|O_NOFOLLOW|O_CLOEXEC);
+ if (node_fd < 0) {
+ if (errno == ENOENT) {
+ log_device_debug_errno(dev, errno, "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, errno, "Cannot open node %s: %m", devnode);
+ }
+
+ if (fstat(node_fd, &stats) < 0)
+ return log_device_debug_errno(dev, errno, "cannot stat() node %s: %m", devnode);
+
+ if ((mode != MODE_INVALID && (stats.st_mode & S_IFMT) != (mode & S_IFMT)) || stats.st_rdev != devnum) {
+ log_device_debug(dev, "Found node '%s' with non-matching devnum %s, skipping handling.",
+ devnode, id_filename);
+ return 0; /* We might process a device that already got replaced by the time we have a look
+ * at it, handle this gracefully and step away. */
+ }
+
+ 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_fd(node_fd, 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 r;
+}
+
+static int xsprintf_dev_num_path_from_sd_device(sd_device *dev, char **ret) {
+ char filename[DEV_NUM_PATH_MAX], *s;
+ const char *subsystem;
+ dev_t devnum;
+ int r;
+
+ 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;
+
+ xsprintf_dev_num_path(filename,
+ streq(subsystem, "block") ? "block" : "char",
+ devnum);
+
+ s = strdup(filename);
+ if (!s)
+ return -ENOMEM;
+
+ *ret = s;
+ return 0;
+}
+
+int udev_node_add(sd_device *dev, bool apply,
+ mode_t mode, uid_t uid, gid_t gid,
+ OrderedHashmap *seclabel_list) {
+ const char *devnode, *devlink;
+ _cleanup_free_ char *filename = NULL;
+ int r;
+
+ assert(dev);
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devnode: %m");
+
+ if (DEBUG_LOGGING) {
+ const char *id_filename = NULL;
+
+ (void) device_get_id_filename(dev, &id_filename);
+ log_device_debug(dev, "Handling device node '%s', devnum=%s", devnode, strnull(id_filename));
+ }
+
+ r = node_permissions_apply(dev, apply, mode, uid, gid, seclabel_list);
+ if (r < 0)
+ return r;
+
+ r = xsprintf_dev_num_path_from_sd_device(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 */
+ (void) node_symlink(dev, devnode, filename);
+
+ /* create/update symlinks, add symlinks to name index */
+ FOREACH_DEVICE_DEVLINK(dev, devlink) {
+ r = link_update(dev, devlink, true);
+ if (r < 0)
+ log_device_info_errno(dev, r, "Failed to update device symlinks: %m");
+ }
+
+ return 0;
+}
+
+int udev_node_remove(sd_device *dev) {
+ _cleanup_free_ char *filename = NULL;
+ const char *devlink;
+ int r;
+
+ assert(dev);
+
+ /* remove/update symlinks, remove symlinks from name index */
+ FOREACH_DEVICE_DEVLINK(dev, devlink) {
+ r = link_update(dev, devlink, false);
+ if (r < 0)
+ log_device_info_errno(dev, r, "Failed to update device symlinks: %m");
+ }
+
+ r = xsprintf_dev_num_path_from_sd_device(dev, &filename);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device path: %m");
+
+ /* remove /dev/{block,char}/$major:$minor */
+ (void) unlink(filename);
+
+ return 0;
+}
diff --git a/src/udev/udev-node.h b/src/udev/udev-node.h
new file mode 100644
index 0000000..84c7e45
--- /dev/null
+++ b/src/udev/udev-node.h
@@ -0,0 +1,15 @@
+/* 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_add(sd_device *dev, bool apply,
+ mode_t mode, uid_t uid, gid_t gid,
+ OrderedHashmap *seclabel_list);
+int udev_node_remove(sd_device *dev);
+int udev_node_update_old_links(sd_device *dev, sd_device *dev_old);
diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c
new file mode 100644
index 0000000..ef6a0c1
--- /dev/null
+++ b/src/udev/udev-rules.c
@@ -0,0 +1,2393 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <ctype.h>
+
+#include "alloc-util.h"
+#include "architecture.h"
+#include "conf-files.h"
+#include "def.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 "libudev-util.h"
+#include "list.h"
+#include "mkdir.h"
+#include "nulstr-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "proc-cmdline.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "sysctl-util.h"
+#include "udev-builtin.h"
+#include "udev-event.h"
+#include "udev-rules.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 = -1
+} 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 = -1
+} 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 = -1
+} 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(), util_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_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 = -1,
+} 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 */
+} 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;
+ 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;
+ UdevRuleToken *current_token;
+ LIST_HEAD(UdevRuleToken, tokens);
+ LIST_FIELDS(UdevRuleLine, rule_lines);
+};
+
+struct UdevRuleFile {
+ char *filename;
+ UdevRuleLine *current_line;
+ LIST_HEAD(UdevRuleLine, rule_lines);
+ LIST_FIELDS(UdevRuleFile, rule_files);
+};
+
+struct UdevRules {
+ usec_t dirs_ts_usec;
+ ResolveNameTiming resolve_name_timing;
+ Hashmap *known_users;
+ Hashmap *known_groups;
+ UdevRuleFile *current_file;
+ LIST_HEAD(UdevRuleFile, rule_files);
+};
+
+/*** Logging helpers ***/
+
+#define log_rule_full_errno(device, rules, level, error, fmt, ...) \
+ ({ \
+ UdevRules *_r = (rules); \
+ UdevRuleFile *_f = _r ? _r->current_file : NULL; \
+ UdevRuleLine *_l = _f ? _f->current_line : NULL; \
+ const char *_n = _f ? _f->filename : NULL; \
+ \
+ log_device_full_errno(device, level, error, "%s:%u " fmt, \
+ strna(_n), _l ? _l->line_number : 0, \
+ ##__VA_ARGS__); \
+ })
+
+#define log_rule_full(device, rules, level, ...) (void) log_rule_full_errno(device, rules, level, 0, __VA_ARGS__)
+
+#define log_rule_debug(device, rules, ...) log_rule_full_errno(device, rules, LOG_DEBUG, 0, __VA_ARGS__)
+#define log_rule_info(device, rules, ...) log_rule_full(device, rules, LOG_INFO, __VA_ARGS__)
+#define log_rule_notice(device, rules, ...) log_rule_full(device, rules, LOG_NOTICE, __VA_ARGS__)
+#define log_rule_warning(device, rules, ...) log_rule_full(device, rules, LOG_WARNING, __VA_ARGS__)
+#define log_rule_error(device, rules, ...) log_rule_full(device, rules, LOG_ERR, __VA_ARGS__)
+
+#define log_rule_debug_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_DEBUG, error, __VA_ARGS__)
+#define log_rule_info_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_INFO, error, __VA_ARGS__)
+#define log_rule_notice_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_NOTICE, error, __VA_ARGS__)
+#define log_rule_warning_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_WARNING, error, __VA_ARGS__)
+#define log_rule_error_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_ERR, error, __VA_ARGS__)
+
+#define log_token_full_errno(rules, level, error, ...) log_rule_full_errno(NULL, rules, level, error, __VA_ARGS__)
+#define log_token_full(rules, level, ...) (void) log_token_full_errno(rules, level, 0, __VA_ARGS__)
+
+#define log_token_debug(rules, ...) log_token_full_errno(rules, LOG_DEBUG, 0, __VA_ARGS__)
+#define log_token_info(rules, ...) log_token_full(rules, LOG_INFO, __VA_ARGS__)
+#define log_token_notice(rules, ...) log_token_full(rules, LOG_NOTICE, __VA_ARGS__)
+#define log_token_warning(rules, ...) log_token_full(rules, LOG_WARNING, __VA_ARGS__)
+#define log_token_error(rules, ...) log_token_full(rules, LOG_ERR, __VA_ARGS__)
+
+#define log_token_debug_errno(rules, error, ...) log_token_full_errno(rules, LOG_DEBUG, error, __VA_ARGS__)
+#define log_token_info_errno(rules, error, ...) log_token_full_errno(rules, LOG_INFO, error, __VA_ARGS__)
+#define log_token_notice_errno(rules, error, ...) log_token_full_errno(rules, LOG_NOTICE, error, __VA_ARGS__)
+#define log_token_warning_errno(rules, error, ...) log_token_full_errno(rules, LOG_WARNING, error, __VA_ARGS__)
+#define log_token_error_errno(rules, error, ...) log_token_full_errno(rules, LOG_ERR, error, __VA_ARGS__)
+
+#define _log_token_invalid(rules, key, type) \
+ log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), \
+ "Invalid %s for %s.", type, key)
+
+#define log_token_invalid_op(rules, key) _log_token_invalid(rules, key, "operator")
+#define log_token_invalid_attr(rules, key) _log_token_invalid(rules, key, "attribute")
+
+#define log_token_invalid_attr_format(rules, key, attr, offset, hint) \
+ log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), \
+ "Invalid attribute \"%s\" for %s (char %zu: %s), ignoring, but please fix it.", \
+ attr, key, offset, hint)
+#define log_token_invalid_value(rules, key, value, offset, hint) \
+ log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), \
+ "Invalid value \"%s\" for %s (char %zu: %s), ignoring, but please fix it.", \
+ value, key, offset, hint)
+
+static void log_unknown_owner(sd_device *dev, UdevRules *rules, int error, const char *entity, const char *name) {
+ if (IN_SET(abs(error), ENOENT, ESRCH))
+ log_rule_error(dev, rules, "Unknown %s '%s', ignoring", entity, name);
+ else
+ log_rule_error_errno(dev, rules, error, "Failed to resolve %s '%s', ignoring: %m", entity, name);
+}
+
+/*** Other functions ***/
+
+static void udev_rule_token_free(UdevRuleToken *token) {
+ free(token);
+}
+
+static void udev_rule_line_clear_tokens(UdevRuleLine *rule_line) {
+ UdevRuleToken *i, *next;
+
+ assert(rule_line);
+
+ LIST_FOREACH_SAFE(tokens, i, next, rule_line->tokens)
+ udev_rule_token_free(i);
+
+ rule_line->tokens = NULL;
+}
+
+static void udev_rule_line_free(UdevRuleLine *rule_line) {
+ if (!rule_line)
+ return;
+
+ udev_rule_line_clear_tokens(rule_line);
+
+ if (rule_line->rule_file) {
+ if (rule_line->rule_file->current_line == rule_line)
+ rule_line->rule_file->current_line = rule_line->rule_lines_prev;
+
+ LIST_REMOVE(rule_lines, rule_line->rule_file->rule_lines, rule_line);
+ }
+
+ free(rule_line->line);
+ free(rule_line);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleLine*, udev_rule_line_free);
+
+static void udev_rule_file_free(UdevRuleFile *rule_file) {
+ UdevRuleLine *i, *next;
+
+ if (!rule_file)
+ return;
+
+ LIST_FOREACH_SAFE(rule_lines, i, next, rule_file->rule_lines)
+ udev_rule_line_free(i);
+
+ free(rule_file->filename);
+ free(rule_file);
+}
+
+UdevRules *udev_rules_free(UdevRules *rules) {
+ UdevRuleFile *i, *next;
+
+ if (!rules)
+ return NULL;
+
+ LIST_FOREACH_SAFE(rule_files, i, next, rules->rule_files)
+ udev_rule_file_free(i);
+
+ hashmap_free_free_key(rules->known_users);
+ hashmap_free_free_key(rules->known_groups);
+ return mfree(rules);
+}
+
+static int rule_resolve_user(UdevRules *rules, const char *name, uid_t *ret) {
+ _cleanup_free_ char *n = NULL;
+ uid_t uid;
+ void *val;
+ int r;
+
+ assert(rules);
+ assert(name);
+
+ val = hashmap_get(rules->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, rules, r, "user", name);
+ *ret = UID_INVALID;
+ return 0;
+ }
+
+ n = strdup(name);
+ if (!n)
+ return -ENOMEM;
+
+ r = hashmap_ensure_allocated(&rules->known_users, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(rules->known_users, n, UID_TO_PTR(uid));
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ *ret = uid;
+ return 0;
+}
+
+static int rule_resolve_group(UdevRules *rules, const char *name, gid_t *ret) {
+ _cleanup_free_ char *n = NULL;
+ gid_t gid;
+ void *val;
+ int r;
+
+ assert(rules);
+ assert(name);
+
+ val = hashmap_get(rules->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, rules, r, "group", name);
+ *ret = GID_INVALID;
+ return 0;
+ }
+
+ n = strdup(name);
+ if (!n)
+ return -ENOMEM;
+
+ r = hashmap_ensure_allocated(&rules->known_groups, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(rules->known_groups, 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 void rule_line_append_token(UdevRuleLine *rule_line, UdevRuleToken *token) {
+ assert(rule_line);
+ assert(token);
+
+ if (rule_line->current_token)
+ LIST_APPEND(tokens, rule_line->current_token, token);
+ else
+ LIST_APPEND(tokens, rule_line->tokens, token);
+
+ rule_line->current_token = token;
+}
+
+static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, UdevRuleOperatorType op, char *value, void *data) {
+ UdevRuleToken *token;
+ 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 < TK_M_TEST || type == TK_M_RESULT) {
+ /* 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_append_token(rule_line, 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);
+
+ return 0;
+}
+
+static void check_value_format_and_warn(UdevRules *rules, const char *key, const char *value, bool nonempty) {
+ size_t offset;
+ const char *hint;
+
+ if (nonempty && isempty(value))
+ log_token_invalid_value(rules, key, value, (size_t) 0, "empty value");
+ else if (udev_check_format(value, &offset, &hint) < 0)
+ log_token_invalid_value(rules, key, value, offset + 1, hint);
+}
+
+static int check_attr_format_and_warn(UdevRules *rules, const char *key, const char *value) {
+ size_t offset;
+ const char *hint;
+
+ if (isempty(value))
+ return log_token_invalid_attr(rules, key);
+ if (udev_check_format(value, &offset, &hint) < 0)
+ log_token_invalid_attr_format(rules, key, value, offset + 1, hint);
+ return 0;
+}
+
+static int parse_token(UdevRules *rules, const char *key, char *attr, UdevRuleOperatorType op, char *value) {
+ bool is_match = IN_SET(op, OP_MATCH, OP_NOMATCH);
+ UdevRuleLine *rule_line;
+ int r;
+
+ assert(rules);
+ assert(rules->current_file);
+ assert(rules->current_file->current_line);
+ assert(key);
+ assert(value);
+
+ rule_line = rules->current_file->current_line;
+
+ if (streq(key, "ACTION")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ r = rule_line_add_token(rule_line, TK_M_ACTION, op, value, NULL);
+ } else if (streq(key, "DEVPATH")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ r = rule_line_add_token(rule_line, TK_M_DEVPATH, op, value, NULL);
+ } else if (streq(key, "KERNEL")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ r = rule_line_add_token(rule_line, TK_M_KERNEL, op, value, NULL);
+ } else if (streq(key, "SYMLINK")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+
+ if (!is_match) {
+ check_value_format_and_warn(rules, 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_token_invalid_attr(rules, key);
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (op == OP_ADD) {
+ log_token_warning(rules, "%s key takes '==', '!=', '=', or ':=' operator, assuming '=', but please fix it.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ if (streq(value, "%k"))
+ return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL),
+ "NAME=\"%%k\" is ignored, as it breaks kernel supplied names.");
+ if (isempty(value))
+ return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL),
+ "Ignoring NAME=\"\", as udev will not delete any device nodes.");
+ check_value_format_and_warn(rules, 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_token_invalid_attr(rules, key);
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (op == OP_ASSIGN_FINAL) {
+ log_token_warning(rules, "%s key takes '==', '!=', '=', or '+=' operator, assuming '=', but please fix it.", 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_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL),
+ "Invalid ENV attribute. '%s' cannot be set.", attr);
+
+ check_value_format_and_warn(rules, 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_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+ r = rule_line_add_token(rule_line, TK_M_CONST, op, value, attr);
+ } else if (streq(key, "TAG")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (op == OP_ASSIGN_FINAL) {
+ log_token_warning(rules, "%s key takes '==', '!=', '=', or '+=' operator, assuming '=', but please fix it.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ check_value_format_and_warn(rules, 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_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ if (STR_IN_SET(value, "bus", "class"))
+ log_token_warning(rules, "'%s' must be specified as 'subsystem'; please fix it", value);
+
+ r = rule_line_add_token(rule_line, TK_M_SUBSYSTEM, op, value, NULL);
+ } else if (streq(key, "DRIVER")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, 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(rules, key, attr);
+ if (r < 0)
+ return r;
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) {
+ log_token_warning(rules, "%s key takes '==', '!=', or '=' operator, assuming '=', but please fix it.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ check_value_format_and_warn(rules, 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(rules, key, attr);
+ if (r < 0)
+ return r;
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) {
+ log_token_warning(rules, "%s key takes '==', '!=', or '=' operator, assuming '=', but please fix it.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ check_value_format_and_warn(rules, 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_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_KERNEL, op, value, NULL);
+ } else if (streq(key, "SUBSYSTEMS")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_SUBSYSTEM, op, value, NULL);
+ } else if (streq(key, "DRIVERS")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, 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(rules, key, attr);
+ if (r < 0)
+ return r;
+ if (!is_match)
+ return log_token_invalid_op(rules, key);
+
+ if (startswith(attr, "device/"))
+ log_token_warning(rules, "'device' link may not be available in future kernels; please fix it.");
+ if (strstr(attr, "../"))
+ log_token_warning(rules, "Direct reference to parent sysfs directory, may break in future kernels; please fix it.");
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_ATTR, op, value, attr);
+ } else if (streq(key, "TAGS")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, 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_token_error_errno(rules, r, "Failed to parse mode '%s': %m", attr);
+ }
+ check_value_format_and_warn(rules, key, value, true);
+ if (!is_match)
+ return log_token_invalid_op(rules, 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_token_invalid_attr(rules, key);
+ check_value_format_and_warn(rules, key, value, true);
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, 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_token_invalid_attr(rules, key);
+ check_value_format_and_warn(rules, key, value, true);
+ if (op == OP_REMOVE)
+ return log_token_invalid_op(rules, 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_token_debug(rules,"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_token_error_errno(rules, 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_token_invalid_attr(rules, key);
+ } else if (streq(key, "RESULT")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (!is_match)
+ return log_token_invalid_op(rules, 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_token_invalid_attr(rules, key);
+ if (is_match || op == OP_REMOVE)
+ return log_token_invalid_op(rules, 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_token_error_errno(rules, 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 {
+ log_token_warning(rules, "Invalid value for OPTIONS key, ignoring: '%s'", value);
+ return 0;
+ }
+ } else if (streq(key, "OWNER")) {
+ uid_t uid;
+
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (is_match || op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (op == OP_ADD) {
+ log_token_warning(rules, "%s key takes '=' or ':=' operator, assuming '=', but please fix it.", 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 (rules->resolve_name_timing == RESOLVE_NAME_EARLY &&
+ rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) {
+ r = rule_resolve_user(rules, value, &uid);
+ if (r < 0)
+ return log_token_error_errno(rules, 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 (rules->resolve_name_timing != RESOLVE_NAME_NEVER) {
+ check_value_format_and_warn(rules, key, value, true);
+ r = rule_line_add_token(rule_line, TK_A_OWNER, op, value, NULL);
+ } else {
+ log_token_debug(rules, "User name resolution is disabled, ignoring %s=%s", key, value);
+ return 0;
+ }
+ } else if (streq(key, "GROUP")) {
+ gid_t gid;
+
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (is_match || op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (op == OP_ADD) {
+ log_token_warning(rules, "%s key takes '=' or ':=' operator, assuming '=', but please fix it.", 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 (rules->resolve_name_timing == RESOLVE_NAME_EARLY &&
+ rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) {
+ r = rule_resolve_group(rules, value, &gid);
+ if (r < 0)
+ return log_token_error_errno(rules, 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 (rules->resolve_name_timing != RESOLVE_NAME_NEVER) {
+ check_value_format_and_warn(rules, key, value, true);
+ r = rule_line_add_token(rule_line, TK_A_GROUP, op, value, NULL);
+ } else {
+ log_token_debug(rules, "Resolving group name is disabled, ignoring %s=%s", key, value);
+ return 0;
+ }
+ } else if (streq(key, "MODE")) {
+ mode_t mode;
+
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (is_match || op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (op == OP_ADD) {
+ log_token_warning(rules, "%s key takes '=' or ':=' operator, assuming '=', but please fix it.", 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(rules, 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_token_invalid_attr(rules, key);
+ check_value_format_and_warn(rules, key, value, true);
+ if (is_match || op == OP_REMOVE)
+ return log_token_invalid_op(rules, key);
+ if (op == OP_ASSIGN_FINAL) {
+ log_token_warning(rules, "%s key takes '=' or '+=' operator, assuming '=', but please fix it.", 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_token_invalid_op(rules, key);
+ check_value_format_and_warn(rules, 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_token_error_errno(rules, 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_token_invalid_attr(rules, key);
+ } else if (streq(key, "GOTO")) {
+ if (attr)
+ return log_token_invalid_attr(rules, key);
+ if (op != OP_ASSIGN)
+ return log_token_invalid_op(rules, key);
+ if (FLAGS_SET(rule_line->type, LINE_HAS_GOTO)) {
+ log_token_warning(rules, "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_token_invalid_attr(rules, key);
+ if (op != OP_ASSIGN)
+ return log_token_invalid_op(rules, key);
+
+ rule_line->label = value;
+ SET_FLAG(rule_line->type, LINE_HAS_LABEL, true);
+ return 1;
+ } else
+ return log_token_error_errno(rules, 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 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 sort_tokens(UdevRuleLine *rule_line) {
+ UdevRuleToken *head_old;
+
+ assert(rule_line);
+
+ head_old = TAKE_PTR(rule_line->tokens);
+ rule_line->current_token = NULL;
+
+ while (!LIST_IS_EMPTY(head_old)) {
+ UdevRuleToken *t, *min_token = NULL;
+
+ LIST_FOREACH(tokens, t, head_old)
+ if (!min_token || min_token->type > t->type)
+ min_token = t;
+
+ LIST_REMOVE(tokens, head_old, min_token);
+ rule_line_append_token(rule_line, min_token);
+ }
+}
+
+static int rule_add_line(UdevRules *rules, const char *line_str, unsigned line_nr) {
+ _cleanup_(udev_rule_line_freep) UdevRuleLine *rule_line = NULL;
+ _cleanup_free_ char *line = NULL;
+ UdevRuleFile *rule_file;
+ char *p;
+ int r;
+
+ assert(rules);
+ assert(rules->current_file);
+ assert(line_str);
+
+ rule_file = rules->current_file;
+
+ if (isempty(line_str))
+ return 0;
+
+ /* We use memdup_suffix0() here, since we want to add a second NUL byte to the end, since possibly
+ * some parsers might turn this into a "nulstr", which requires an extra NUL at the end. */
+ line = memdup_suffix0(line_str, strlen(line_str) + 1);
+ 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,
+ };
+
+ if (rule_file->current_line)
+ LIST_APPEND(rule_lines, rule_file->current_line, rule_line);
+ else
+ LIST_APPEND(rule_lines, rule_file->rule_lines, rule_line);
+
+ rule_file->current_line = rule_line;
+
+ for (p = rule_line->line; !isempty(p); ) {
+ char *key, *attr, *value;
+ UdevRuleOperatorType op;
+
+ r = parse_line(&p, &key, &attr, &op, &value);
+ if (r < 0)
+ return log_token_error_errno(rules, r, "Invalid key/value pair, ignoring.");
+ if (r == 0)
+ break;
+
+ r = parse_token(rules, key, attr, op, value);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule_line->type == 0) {
+ log_token_warning(rules, "The line takes no effect, ignoring.");
+ return 0;
+ }
+
+ sort_tokens(rule_line);
+ TAKE_PTR(rule_line);
+ return 0;
+}
+
+static void rule_resolve_goto(UdevRuleFile *rule_file) {
+ UdevRuleLine *line, *line_next, *i;
+
+ assert(rule_file);
+
+ /* link GOTOs to LABEL rules in this file to be able to fast-forward */
+ LIST_FOREACH_SAFE(rule_lines, line, line_next, rule_file->rule_lines) {
+ if (!FLAGS_SET(line->type, LINE_HAS_GOTO))
+ continue;
+
+ LIST_FOREACH_AFTER(rule_lines, i, line)
+ if (streq_ptr(i->label, line->goto_label)) {
+ line->goto_line = i;
+ break;
+ }
+
+ if (!line->goto_line) {
+ log_error("%s:%u: GOTO=\"%s\" has no matching label, ignoring",
+ rule_file->filename, line->line_number, line->goto_label);
+
+ SET_FLAG(line->type, LINE_HAS_GOTO, false);
+ line->goto_label = NULL;
+
+ if ((line->type & ~LINE_HAS_LABEL) == 0) {
+ log_notice("%s:%u: The line takes no effect any more, dropping",
+ rule_file->filename, line->line_number);
+ if (line->type == LINE_HAS_LABEL)
+ udev_rule_line_clear_tokens(line);
+ else
+ udev_rule_line_free(line);
+ }
+ }
+ }
+}
+
+int udev_rules_parse_file(UdevRules *rules, const char *filename) {
+ _cleanup_free_ char *continuation = NULL, *name = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ UdevRuleFile *rule_file;
+ bool ignore_line = false;
+ unsigned line_nr = 0;
+ int r;
+
+ f = fopen(filename, "re");
+ if (!f) {
+ if (errno == ENOENT)
+ return 0;
+
+ return -errno;
+ }
+
+ (void) fd_warn_permissions(filename, fileno(f));
+
+ if (null_or_empty_fd(fileno(f))) {
+ log_debug("Skipping empty file: %s", filename);
+ return 0;
+ }
+
+ 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),
+ };
+
+ if (rules->current_file)
+ LIST_APPEND(rule_files, rules->current_file, rule_file);
+ else
+ LIST_APPEND(rule_files, rules->rule_files, rule_file);
+
+ rules->current_file = rule_file;
+
+ for (;;) {
+ _cleanup_free_ char *buf = NULL;
+ size_t len;
+ char *line;
+
+ r = read_line(f, UTIL_LINE_SIZE, &buf);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ line_nr++;
+ line = skip_leading_chars(buf, NULL);
+
+ if (line[0] == '#')
+ continue;
+
+ len = strlen(line);
+
+ if (continuation && !ignore_line) {
+ if (strlen(continuation) + len >= UTIL_LINE_SIZE)
+ ignore_line = true;
+
+ if (!strextend(&continuation, line, NULL))
+ 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_error("%s:%u: Line is too long, ignored", filename, line_nr);
+ else if (len > 0)
+ (void) rule_add_line(rules, line, line_nr);
+
+ continuation = mfree(continuation);
+ ignore_line = false;
+ }
+
+ rule_resolve_goto(rule_file);
+ return 0;
+}
+
+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;
+ char **f;
+ int r;
+
+ rules = udev_rules_new(resolve_name_timing);
+ if (!rules)
+ return -ENOMEM;
+
+ (void) udev_rules_check_timestamp(rules);
+
+ 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);
+ 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_check_timestamp(UdevRules *rules) {
+ if (!rules)
+ return false;
+
+ return paths_check_timestamp(RULES_DIRS, &rules->dirs_ts_usec, true);
+}
+
+static bool token_match_string(UdevRuleToken *token, const char *str) {
+ const char *i, *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("Invalid match type");
+ }
+
+ return token->op == (match ? OP_MATCH : OP_NOMATCH);
+}
+
+static bool token_match_attr(UdevRuleToken *token, sd_device *dev, UdevEvent *event) {
+ char nbuf[UTIL_NAME_SIZE], vbuf[UTIL_NAME_SIZE];
+ const char *name, *value;
+
+ assert(token);
+ 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);
+ 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 (util_resolve_subsys_kernel(name, vbuf, sizeof(vbuf), true) < 0)
+ return false;
+ value = vbuf;
+ break;
+ default:
+ assert_not_reached("Invalid attribute substitution type");
+ }
+
+ /* 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) {
+ const char *key, *val;
+ 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 UTIL_PATH_SIZE]) {
+ _cleanup_closedir_ DIR *dir = NULL;
+ struct dirent *dent;
+ char buf[UTIL_PATH_SIZE], *p;
+ const char *tail;
+ size_t len, size;
+
+ 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(&p, size, attr, len);
+
+ dir = opendir(buf);
+ if (!dir)
+ return -errno;
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ if (dent->d_name[0] == '.')
+ continue;
+
+ strscpyl(p, size, dent->d_name, tail, NULL);
+ if (faccessat(dirfd(dir), p, F_OK, 0) < 0)
+ continue;
+
+ strcpy(attr, buf);
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int udev_rule_apply_token_to_event(
+ UdevRules *rules,
+ sd_device *dev,
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list) {
+
+ UdevRuleToken *token;
+ char buf[UTIL_PATH_SIZE];
+ const char *val;
+ size_t count;
+ bool match;
+ int r;
+
+ assert(rules);
+ 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. */
+
+ token = rules->current_file->current_line->current_token;
+
+ switch (token->type) {
+ case TK_M_ACTION: {
+ DeviceAction a;
+
+ r = device_get_action(dev, &a);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to get uevent action type: %m");
+
+ return token_match_string(token, device_action_to_string(a));
+ }
+ case TK_M_DEVPATH:
+ r = sd_device_get_devpath(dev, &val);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to get devpath: %m");
+
+ return token_match_string(token, val);
+ case TK_M_KERNEL:
+ case TK_M_PARENTS_KERNEL:
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, 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/"))))
+ 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:
+ if (sd_device_get_property_value(dev, token->data, &val) < 0)
+ val = hashmap_get(properties_list, token->data);
+
+ return token_match_string(token, val);
+ case TK_M_CONST: {
+ const char *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
+ assert_not_reached("Invalid CONST key");
+ return token_match_string(token, val);
+ }
+ case TK_M_TAG:
+ case TK_M_PARENTS_TAG:
+ FOREACH_DEVICE_TAG(dev, val)
+ if (token_match_string(token, val))
+ return token->op == OP_MATCH;
+ return token->op == OP_NOMATCH;
+ case TK_M_SUBSYSTEM:
+ case TK_M_PARENTS_SUBSYSTEM:
+ r = sd_device_get_subsystem(dev, &val);
+ if (r == -ENOENT)
+ val = NULL;
+ else if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to get subsystem: %m");
+
+ return token_match_string(token, val);
+ case TK_M_DRIVER:
+ case TK_M_PARENTS_DRIVER:
+ r = sd_device_get_driver(dev, &val);
+ if (r == -ENOENT)
+ val = NULL;
+ else if (r < 0)
+ return log_rule_error_errno(dev, rules, 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;
+
+ (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false);
+ r = sysctl_read(sysctl_normalize(buf), &value);
+ if (r < 0 && r != -ENOENT)
+ return log_rule_error_errno(dev, rules, 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);
+ struct stat statbuf;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ if (!path_is_absolute(buf) &&
+ util_resolve_subsys_kernel(buf, buf, sizeof(buf), false) < 0) {
+ char tmp[UTIL_PATH_SIZE];
+
+ r = sd_device_get_syspath(dev, &val);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to get syspath: %m");
+
+ strscpy(tmp, sizeof(tmp), buf);
+ strscpyl(buf, sizeof(buf), val, "/", tmp, NULL);
+ }
+
+ r = attr_subst_subdir(buf);
+ if (r == -ENOENT)
+ return token->op == OP_NOMATCH;
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, 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 result[UTIL_LINE_SIZE];
+
+ event->program_result = mfree(event->program_result);
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ log_rule_debug(dev, rules, "Running PROGRAM '%s'", buf);
+
+ r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof(result));
+ if (r != 0) {
+ if (r < 0)
+ log_rule_warning_errno(dev, rules, r, "Failed to execute \"%s\": %m", buf);
+ else /* returned value is positive when program fails */
+ log_rule_debug(dev, rules, "Command \"%s\" returned %d (error)", buf, r);
+ return token->op == OP_NOMATCH;
+ }
+
+ delete_trailing_chars(result, "\n");
+ count = util_replace_chars(result, UDEV_ALLOWED_CHARS_INPUT);
+ if (count > 0)
+ log_rule_debug(dev, rules, "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;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ log_rule_debug(dev, rules, "Importing properties from '%s'", buf);
+
+ f = fopen(buf, "re");
+ if (!f) {
+ if (errno != ENOENT)
+ return log_rule_error_errno(dev, rules, 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_rule_debug_errno(dev, rules, 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_rule_debug_errno(dev, rules, 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_rule_error_errno(dev, rules, r,
+ "Failed to add property %s=%s: %m",
+ key, value);
+ }
+
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_PROGRAM: {
+ char result[UTIL_LINE_SIZE], *line, *pos;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ log_rule_debug(dev, rules, "Importing properties from results of '%s'", buf);
+
+ r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof result);
+ if (r != 0) {
+ if (r < 0)
+ log_rule_warning_errno(dev, rules, r, "Failed to execute '%s', ignoring: %m", buf);
+ else /* returned value is positive when program fails */
+ log_rule_debug(dev, rules, "Command \"%s\" returned %d (error), ignoring", buf, r);
+ return token->op == OP_NOMATCH;
+ }
+
+ for (line = result; !isempty(line); line = pos) {
+ char *key, *value;
+
+ pos = strchr(line, '\n');
+ if (pos)
+ *pos++ = '\0';
+
+ r = get_property_from_string(line, &key, &value);
+ if (r < 0) {
+ log_rule_debug_errno(dev, rules, 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_rule_error_errno(dev, rules, 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);
+ unsigned mask = 1U << (int) cmd;
+
+ if (udev_builtin_run_once(cmd)) {
+ /* check if we ran already */
+ if (event->builtin_run & mask) {
+ log_rule_debug(dev, rules, "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);
+ log_rule_debug(dev, rules, "Importing properties from results of builtin command '%s'", buf);
+
+ r = udev_builtin_run(dev, cmd, buf, false);
+ if (r < 0) {
+ /* remember failure */
+ log_rule_debug_errno(dev, rules, 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: {
+ 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_rule_error_errno(dev, rules, r,
+ "Failed to get property '%s' from database: %m",
+ token->value);
+
+ r = device_add_property(dev, token->value, val);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, 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_rule_error_errno(dev, rules, 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_rule_error_errno(dev, rules, r, "Failed to add property '%s=%s': %m",
+ token->value, value ?: "1");
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_PARENT: {
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ r = import_parent_into_properties(dev, buf);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, 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_OWNER: {
+ char owner[UTIL_NAME_SIZE];
+ const char *ow = owner;
+
+ 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);
+ r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
+ if (r < 0)
+ log_unknown_owner(dev, rules, r, "user", owner);
+ else
+ log_rule_debug(dev, rules, "OWNER %s(%u)", owner, event->uid);
+ break;
+ }
+ case TK_A_GROUP: {
+ char group[UTIL_NAME_SIZE];
+ const char *gr = group;
+
+ 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);
+ r = get_group_creds(&gr, &event->gid, USER_CREDS_ALLOW_MISSING);
+ if (r < 0)
+ log_unknown_owner(dev, rules, r, "group", group);
+ else
+ log_rule_debug(dev, rules, "GROUP %s(%u)", group, event->gid);
+ break;
+ }
+ case TK_A_MODE: {
+ char mode_str[UTIL_NAME_SIZE];
+
+ 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);
+ r = parse_mode(mode_str, &event->mode);
+ if (r < 0)
+ log_rule_error_errno(dev, rules, r, "Failed to parse mode '%s', ignoring: %m", mode_str);
+ else
+ log_rule_debug(dev, rules, "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_rule_debug(dev, rules, "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_rule_debug(dev, rules, "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_rule_debug(dev, rules, "MODE %#o", event->mode);
+ break;
+ case TK_A_SECLABEL: {
+ _cleanup_free_ char *name = NULL, *label = NULL;
+ char label_str[UTIL_LINE_SIZE] = {};
+
+ name = strdup(token->data);
+ if (!name)
+ return log_oom();
+
+ (void) udev_event_apply_format(event, token->value, label_str, sizeof(label_str), false);
+ 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_allocated(&event->seclabel_list, NULL);
+ if (r < 0)
+ return log_oom();
+
+ r = ordered_hashmap_put(event->seclabel_list, name, label);
+ if (r < 0)
+ return log_oom();
+ log_rule_debug(dev, rules, "SECLABEL{%s}='%s'", name, label);
+ name = label = NULL;
+ break;
+ }
+ case TK_A_ENV: {
+ const char *name = token->data;
+ char value_new[UTIL_NAME_SIZE], *p = value_new;
+ size_t l = sizeof(value_new);
+
+ if (isempty(token->value)) {
+ if (token->op == OP_ADD)
+ break;
+ r = device_add_property(dev, name, NULL);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to remove property '%s': %m", name);
+ break;
+ }
+
+ if (token->op == OP_ADD &&
+ sd_device_get_property_value(dev, name, &val) >= 0)
+ l = strpcpyl(&p, l, val, " ", NULL);
+
+ (void) udev_event_apply_format(event, token->value, p, l, false);
+
+ r = device_add_property(dev, name, value_new);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to add property '%s=%s': %m", name, value_new);
+ break;
+ }
+ case TK_A_TAG: {
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ if (token->op == OP_ASSIGN)
+ device_cleanup_tags(dev);
+
+ if (buf[strspn(buf, ALPHANUMERICAL "-_")] != '\0') {
+ log_rule_error(dev, rules, "Invalid tag name '%s', ignoring", buf);
+ break;
+ }
+ if (token->op == OP_REMOVE)
+ device_remove_tag(dev, buf);
+ else {
+ r = device_add_tag(dev, buf, true);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to add tag '%s': %m", buf);
+ }
+ break;
+ }
+ case TK_A_NAME: {
+ if (event->name_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->name_final = true;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+ if (IN_SET(event->esc, ESCAPE_UNSET, ESCAPE_REPLACE)) {
+ count = util_replace_chars(buf, "/");
+ if (count > 0)
+ log_rule_debug(dev, rules, "Replaced %zu character(s) from result of NAME=\"%s\"",
+ count, token->value);
+ }
+ if (sd_device_get_devnum(dev, NULL) >= 0 &&
+ (sd_device_get_devname(dev, &val) < 0 ||
+ !streq_ptr(buf, path_startswith(val, "/dev/")))) {
+ log_rule_error(dev, rules,
+ "Kernel device nodes cannot be renamed, ignoring NAME=\"%s\"; please fix it.",
+ token->value);
+ break;
+ }
+ if (free_and_strdup(&event->name, buf) < 0)
+ return log_oom();
+
+ log_rule_debug(dev, rules, "NAME '%s'", event->name);
+ break;
+ }
+ case TK_A_DEVLINK: {
+ char *p;
+
+ 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);
+
+ /* allow multiple symlinks separated by spaces */
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), event->esc != ESCAPE_NONE);
+ if (event->esc == ESCAPE_UNSET)
+ count = util_replace_chars(buf, "/ ");
+ else if (event->esc == ESCAPE_REPLACE)
+ count = util_replace_chars(buf, "/");
+ else
+ count = 0;
+ if (count > 0)
+ log_rule_debug(dev, rules, "Replaced %zu character(s) from result of LINK", count);
+
+ p = skip_leading_chars(buf, NULL);
+ while (!isempty(p)) {
+ char filename[UTIL_PATH_SIZE], *next;
+
+ next = strchr(p, ' ');
+ if (next) {
+ *next++ = '\0';
+ next = skip_leading_chars(next, NULL);
+ }
+
+ strscpyl(filename, sizeof(filename), "/dev/", p, NULL);
+ r = device_add_devlink(dev, filename);
+ if (r < 0)
+ return log_rule_error_errno(dev, rules, r, "Failed to add devlink '%s': %m", filename);
+
+ log_rule_debug(dev, rules, "LINK '%s'", p);
+ p = next;
+ }
+ break;
+ }
+ case TK_A_ATTR: {
+ const char *key_name = token->data;
+ char value[UTIL_NAME_SIZE];
+
+ if (util_resolve_subsys_kernel(key_name, buf, sizeof(buf), false) < 0 &&
+ sd_device_get_syspath(dev, &val) >= 0)
+ strscpyl(buf, sizeof(buf), val, "/", key_name, NULL);
+
+ r = attr_subst_subdir(buf);
+ if (r < 0) {
+ log_rule_error_errno(dev, rules, r, "Could not find file matches '%s', ignoring: %m", buf);
+ break;
+ }
+ (void) udev_event_apply_format(event, token->value, value, sizeof(value), false);
+
+ log_rule_debug(dev, rules, "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);
+ if (r < 0)
+ log_rule_error_errno(dev, rules, r, "Failed to write ATTR{%s}, ignoring: %m", buf);
+ break;
+ }
+ case TK_A_SYSCTL: {
+ char value[UTIL_NAME_SIZE];
+
+ (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false);
+ (void) udev_event_apply_format(event, token->value, value, sizeof(value), false);
+ sysctl_normalize(buf);
+ log_rule_debug(dev, rules, "SYSCTL '%s' writing '%s'", buf, value);
+ r = sysctl_write(buf, value);
+ if (r < 0)
+ log_rule_error_errno(dev, rules, 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;
+
+ 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);
+
+ r = ordered_hashmap_ensure_allocated(&event->run_list, NULL);
+ if (r < 0)
+ return log_oom();
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false);
+
+ cmd = strdup(buf);
+ if (!cmd)
+ return log_oom();
+
+ r = ordered_hashmap_put(event->run_list, cmd, token->data);
+ if (r < 0)
+ return log_oom();
+
+ TAKE_PTR(cmd);
+
+ log_rule_debug(dev, rules, "RUN '%s'", token->value);
+ break;
+ }
+ case TK_A_OPTIONS_STATIC_NODE:
+ /* do nothing for events. */
+ break;
+ default:
+ assert_not_reached("Invalid token type");
+ }
+
+ 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(
+ UdevRules *rules,
+ UdevEvent *event,
+ int timeout_signal) {
+
+ UdevRuleLine *line;
+ UdevRuleToken *head;
+ int r;
+
+ line = rules->current_file->current_line;
+ head = rules->current_file->current_line->current_token;
+ event->dev_parent = event->dev;
+ for (;;) {
+ LIST_FOREACH(tokens, line->current_token, head) {
+ if (!token_is_for_parents(line->current_token))
+ return true; /* All parent tokens match. */
+ r = udev_rule_apply_token_to_event(rules, event->dev_parent, event, 0, timeout_signal, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+ }
+ if (!line->current_token)
+ /* All parent tokens match. But no assign tokens in the line. Hmm... */
+ 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(
+ UdevRules *rules,
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRuleLine **next_line) {
+
+ UdevRuleLine *line = rules->current_file->current_line;
+ UdevRuleLineType mask = LINE_HAS_GOTO | LINE_UPDATE_SOMETHING;
+ UdevRuleToken *token, *next_token;
+ bool parents_done = false;
+ DeviceAction action;
+ int r;
+
+ r = device_get_action(event->dev, &action);
+ if (r < 0)
+ return r;
+
+ if (action != DEVICE_ACTION_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;
+ LIST_FOREACH_SAFE(tokens, token, next_token, line->tokens) {
+ line->current_token = token;
+
+ if (token_is_for_parents(token)) {
+ if (parents_done)
+ continue;
+
+ r = udev_rule_apply_parent_token_to_event(rules, event, timeout_signal);
+ if (r <= 0)
+ return r;
+
+ parents_done = true;
+ continue;
+ }
+
+ r = udev_rule_apply_token_to_event(rules, event->dev, event, timeout_usec, timeout_signal, properties_list);
+ if (r <= 0)
+ return r;
+ }
+
+ if (line->goto_line)
+ *next_line = line->goto_line;
+
+ return 0;
+}
+
+int udev_rules_apply_to_event(
+ UdevRules *rules,
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list) {
+
+ UdevRuleFile *file;
+ UdevRuleLine *next_line;
+ int r;
+
+ assert(rules);
+ assert(event);
+
+ LIST_FOREACH(rule_files, file, rules->rule_files) {
+ rules->current_file = file;
+ LIST_FOREACH_SAFE(rule_lines, file->current_line, next_line, file->rule_lines) {
+ r = udev_rule_apply_line_to_event(rules, event, timeout_usec, timeout_signal, properties_list, &next_line);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+static int apply_static_dev_perms(const char *devnode, uid_t uid, gid_t gid, mode_t mode, char **tags) {
+ char device_node[UTIL_PATH_SIZE], tags_dir[UTIL_PATH_SIZE], tag_symlink[UTIL_PATH_SIZE];
+ _cleanup_free_ char *unescaped_filename = NULL;
+ struct stat stats;
+ char **t;
+ int r;
+
+ assert(devnode);
+
+ if (uid == UID_INVALID && gid == GID_INVALID && mode == MODE_INVALID && !tags)
+ return 0;
+
+ strscpyl(device_node, sizeof(device_node), "/dev/", devnode, NULL);
+ if (stat(device_node, &stats) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to stat %s: %m", device_node);
+ return 0;
+ }
+
+ if (!S_ISBLK(stats.st_mode) && !S_ISCHR(stats.st_mode)) {
+ log_warning("%s is neither block nor character device, ignoring.", device_node);
+ return 0;
+ }
+
+ if (!strv_isempty(tags)) {
+ unescaped_filename = xescape(devnode, "/.");
+ 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) {
+ strscpyl(tags_dir, sizeof(tags_dir), "/run/udev/static_node-tags/", *t, "/", NULL);
+ r = mkdir_p(tags_dir, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create %s: %m", tags_dir);
+
+ strscpyl(tag_symlink, sizeof(tag_symlink), tags_dir, unescaped_filename, NULL);
+ r = symlink(device_node, tag_symlink);
+ if (r < 0 && errno != EEXIST)
+ return log_error_errno(errno, "Failed to create symlink %s -> %s: %m",
+ tag_symlink, device_node);
+ }
+
+ /* don't touch the permissions if only the tags were set */
+ if (uid == UID_INVALID && gid == GID_INVALID && mode == MODE_INVALID)
+ return 0;
+
+ if (mode == MODE_INVALID)
+ mode = gid_is_valid(gid) ? 0660 : 0600;
+ if (!uid_is_valid(uid))
+ uid = 0;
+ if (!gid_is_valid(gid))
+ gid = 0;
+
+ r = chmod_and_chown(device_node, mode, uid, gid);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to chown '%s' %u %u: %m", device_node, uid, gid);
+ else
+ log_debug("chown '%s' %u:%u with mode %#o", device_node, uid, gid, mode);
+
+ (void) utimensat(AT_FDCWD, device_node, NULL, 0);
+ return 0;
+}
+
+static int udev_rule_line_apply_static_dev_perms(UdevRuleLine *rule_line) {
+ UdevRuleToken *token;
+ _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 = apply_static_dev_perms(token->value, uid, gid, mode, tags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int udev_rules_apply_static_dev_perms(UdevRules *rules) {
+ UdevRuleFile *file;
+ UdevRuleLine *line;
+ 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;
+}
diff --git a/src/udev/udev-rules.h b/src/udev/udev-rules.h
new file mode 100644
index 0000000..3f40a53
--- /dev/null
+++ b/src/udev/udev-rules.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "hashmap.h"
+#include "time-util.h"
+#include "udev-util.h"
+
+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 = -1
+} UdevRuleEscapeType;
+
+int udev_rules_parse_file(UdevRules *rules, const char *filename);
+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);
+
+bool udev_rules_check_timestamp(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);
diff --git a/src/udev/udev-watch.c b/src/udev/udev-watch.c
new file mode 100644
index 0000000..8656fb0
--- /dev/null
+++ b/src/udev/udev-watch.c
@@ -0,0 +1,174 @@
+/* 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 <unistd.h>
+
+#include "alloc-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "fs-util.h"
+#include "mkdir.h"
+#include "stdio-util.h"
+#include "udev-watch.h"
+
+static int inotify_fd = -1;
+
+/* inotify descriptor, will be shared with rules directory;
+ * set to cloexec since we need our children to be able to add
+ * watches for us. */
+int udev_watch_init(void) {
+ inotify_fd = inotify_init1(IN_CLOEXEC);
+ if (inotify_fd < 0)
+ return -errno;
+
+ return inotify_fd;
+}
+
+/* Move any old watches directory out of the way, and then restore the watches. */
+int udev_watch_restore(void) {
+ struct dirent *ent;
+ DIR *dir;
+ int r;
+
+ if (inotify_fd < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid inotify descriptor.");
+
+ if (rename("/run/udev/watch", "/run/udev/watch.old") < 0) {
+ if (errno != ENOENT)
+ return log_warning_errno(errno, "Failed to move watches directory /run/udev/watch. Old watches will not be restored: %m");
+
+ return 0;
+ }
+
+ dir = opendir("/run/udev/watch.old");
+ if (!dir)
+ return log_warning_errno(errno, "Failed to open old watches directory /run/udev/watch.old. Old watches will not be restored: %m");
+
+ FOREACH_DIRENT_ALL(ent, dir, break) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ _cleanup_free_ char *device = NULL;
+
+ if (ent->d_name[0] == '.')
+ continue;
+
+ r = readlinkat_malloc(dirfd(dir), ent->d_name, &device);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to read link '/run/udev/watch.old/%s', ignoring: %m", ent->d_name);
+ goto unlink;
+ }
+
+ r = sd_device_new_from_device_id(&dev, device);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to create sd_device object for '%s', ignoring: %m", device);
+ goto unlink;
+ }
+
+ log_device_debug(dev, "Restoring old watch");
+ (void) udev_watch_begin(dev);
+unlink:
+ (void) unlinkat(dirfd(dir), ent->d_name, 0);
+ }
+
+ (void) closedir(dir);
+ (void) rmdir("/run/udev/watch.old");
+
+ return 0;
+}
+
+int udev_watch_begin(sd_device *dev) {
+ char filename[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)];
+ const char *devnode, *id_filename;
+ int wd, r;
+
+ if (inotify_fd < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid inotify descriptor.");
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get device name: %m");
+
+ 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_full_errno(dev, errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to add device '%s' to watch: %m", devnode);
+
+ device_set_watch_handle(dev, wd);
+
+ xsprintf(filename, "/run/udev/watch/%d", wd);
+ r = mkdir_parents(filename, 0755);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to create parent directory of '%s': %m", filename);
+ (void) unlink(filename);
+
+ r = device_get_id_filename(dev, &id_filename);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get device id-filename: %m");
+
+ if (symlink(id_filename, filename) < 0)
+ return log_device_error_errno(dev, errno, "Failed to create symlink %s: %m", filename);
+
+ return 0;
+}
+
+int udev_watch_end(sd_device *dev) {
+ char filename[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)];
+ int wd, r;
+
+ if (inotify_fd < 0)
+ return 0; /* Nothing to do. */
+
+ r = device_get_watch_handle(dev, &wd);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get watch handle, ignoring: %m");
+
+ log_device_debug(dev, "Removing watch");
+ (void) inotify_rm_watch(inotify_fd, wd);
+
+ xsprintf(filename, "/run/udev/watch/%d", wd);
+ (void) unlink(filename);
+
+ device_set_watch_handle(dev, -1);
+
+ return 0;
+}
+
+int udev_watch_lookup(int wd, sd_device **ret) {
+ char filename[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)];
+ _cleanup_free_ char *device = NULL;
+ int r;
+
+ assert(ret);
+
+ if (inotify_fd < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid inotify descriptor.");
+
+ if (wd < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid watch handle.");
+
+ xsprintf(filename, "/run/udev/watch/%d", wd);
+ r = readlink_malloc(filename, &device);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read link '%s': %m", filename);
+
+ r = sd_device_new_from_device_id(ret, device);
+ if (r == -ENODEV)
+ return 0;
+ if (r < 0)
+ return log_debug_errno(r, "Failed to create sd_device object for '%s': %m", device);
+
+ return 1;
+}
diff --git a/src/udev/udev-watch.h b/src/udev/udev-watch.h
new file mode 100644
index 0000000..a15fa27
--- /dev/null
+++ b/src/udev/udev-watch.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "sd-device.h"
+
+int udev_watch_init(void);
+int udev_watch_restore(void);
+int udev_watch_begin(sd_device *dev);
+int udev_watch_end(sd_device *dev);
+int udev_watch_lookup(int wd, sd_device **ret);
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..7b4f400
--- /dev/null
+++ b/src/udev/udev.pc.in
@@ -0,0 +1,6 @@
+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..ef23a6c
--- /dev/null
+++ b/src/udev/udevadm-control.c
@@ -0,0 +1,186 @@
+/* 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 "syslog-util.h"
+#include "time-util.h"
+#include "udevadm.h"
+#include "udev-ctrl.h"
+#include "util.h"
+#include "virt.h"
+
+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;
+}
+
+int control_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *uctrl = NULL;
+ usec_t timeout = 60 * USEC_PER_SEC;
+ int c, r;
+
+ 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' },
+ {}
+ };
+
+ if (running_in_chroot() > 0) {
+ log_info("Running in chroot, ignoring request.");
+ return 0;
+ }
+
+ if (argc <= 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "This command expects one or more options.");
+
+ r = udev_ctrl_new(&uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize udev control: %m");
+
+ while ((c = getopt_long(argc, argv, "el:sSRp:m:t:Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'e':
+ r = udev_ctrl_send_exit(uctrl);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --exit after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send exit request: %m");
+ break;
+ case 'l':
+ r = log_level_from_string(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse log level '%s': %m", optarg);
+
+ r = udev_ctrl_send_set_log_level(uctrl, r);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --log-level after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send request to set log level: %m");
+ break;
+ case 's':
+ r = udev_ctrl_send_stop_exec_queue(uctrl);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --stop-exec-queue after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send request to stop exec queue: %m");
+ break;
+ case 'S':
+ r = udev_ctrl_send_start_exec_queue(uctrl);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --start-exec-queue after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send request to start exec queue: %m");
+ break;
+ case 'R':
+ r = udev_ctrl_send_reload(uctrl);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --reload after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send reload request: %m");
+ break;
+ case 'p':
+ if (!strchr(optarg, '='))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "expect <KEY>=<value> instead of '%s'", optarg);
+
+ r = udev_ctrl_send_set_env(uctrl, optarg);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --property after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send request to update 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 events '%s': %m", optarg);
+
+ r = udev_ctrl_send_set_children_max(uctrl, i);
+ if (r == -ENOANO)
+ log_warning("Cannot specify --children-max after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send request to set number of children: %m");
+ break;
+ }
+ case ARG_PING:
+ r = udev_ctrl_send_ping(uctrl);
+ if (r == -ENOANO)
+ log_error("Cannot specify --ping after --exit, ignoring.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to send a ping message: %m");
+ break;
+ case 't':
+ r = parse_sec(optarg, &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("Unknown option.");
+ }
+
+ if (optind < argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Extraneous argument: %s", argv[optind]);
+
+ r = udev_ctrl_wait(uctrl, 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..3d21922
--- /dev/null
+++ b/src/udev/udevadm-hwdb.c
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "hwdb-util.h"
+#include "udevadm.h"
+#include "util.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("Unknown option");
+ }
+
+ 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.");
+
+ 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);
+
+ return 0;
+}
diff --git a/src/udev/udevadm-info.c b/src/udev/udevadm-info.c
new file mode 100644
index 0000000..5ff6256
--- /dev/null
+++ b/src/udev/udevadm-info.c
@@ -0,0 +1,519 @@
+/* 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 "dirent-util.h"
+#include "fd-util.h"
+#include "sort-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "udev-util.h"
+#include "udevadm-util.h"
+#include "udevadm.h"
+
+typedef enum ActionType {
+ ACTION_QUERY,
+ ACTION_ATTRIBUTE_WALK,
+ ACTION_DEVICE_ID_FILE,
+} ActionType;
+
+typedef enum QueryType {
+ QUERY_NAME,
+ QUERY_PATH,
+ QUERY_SYMLINK,
+ QUERY_PROPERTY,
+ QUERY_ALL,
+} QueryType;
+
+static bool arg_root = false;
+static bool arg_export = false;
+static const char *arg_export_prefix = NULL;
+static usec_t arg_wait_for_initialization_timeout = 0;
+
+static bool skip_attribute(const char *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 int sysattr_compare(const SysAttr *a, const SysAttr *b) {
+ return strcmp(a->name, b->name);
+}
+
+static int print_all_attributes(sd_device *device, bool is_parent) {
+ _cleanup_free_ SysAttr *sysattrs = NULL;
+ size_t n_items = 0, n_allocated = 0;
+ const char *name, *value;
+
+ 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;
+
+ if (sd_device_get_sysattr_value(device, name, &value) < 0)
+ continue;
+
+ /* 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;
+
+ if (!GREEDY_REALLOC(sysattrs, n_allocated, 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;
+
+ 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 *str, *val;
+ int i;
+
+ (void) sd_device_get_devpath(device, &str);
+ printf("P: %s\n", str);
+
+ if (sd_device_get_devname(device, &str) >= 0) {
+ assert_se(val = path_startswith(str, "/dev/"));
+ printf("N: %s\n", val);
+ }
+
+ if (device_get_devlink_priority(device, &i) >= 0)
+ printf("L: %i\n", i);
+
+ FOREACH_DEVICE_DEVLINK(device, str) {
+ assert_se(val = path_startswith(str, "/dev/"));
+ printf("S: %s\n", val);
+ }
+
+ FOREACH_DEVICE_PROPERTY(device, str, val)
+ printf("E: %s=%s\n", str, val);
+
+ puts("");
+ return 0;
+}
+
+static int stat_device(const char *name, bool export, const char *prefix) {
+ struct stat statbuf;
+
+ 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(void) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ sd_device *d;
+ int r;
+
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return log_oom();
+
+ r = sd_device_enumerator_allow_uninitialized(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set allowing uninitialized flag: %m");
+
+ r = device_enumerator_scan_devices(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to scan devices: %m");
+
+ FOREACH_DEVICE_AND_SUBSYSTEM(e, d)
+ (void) print_record(d);
+
+ return 0;
+}
+
+static void cleanup_dir(DIR *dir, mode_t mask, int depth) {
+ struct dirent *dent;
+
+ if (depth <= 0)
+ return;
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ struct stat stats;
+
+ if (dent->d_name[0] == '.')
+ 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 *dir2 = NULL;
+
+ dir2 = fdopendir(openat(dirfd(dir), dent->d_name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC));
+ if (dir2)
+ cleanup_dir(dir2, mask, depth-1);
+
+ (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, *dir5 = NULL;
+
+ (void) unlink("/run/udev/queue.bin");
+
+ dir1 = opendir("/run/udev/data");
+ if (dir1)
+ cleanup_dir(dir1, S_ISVTX, 1);
+
+ dir2 = opendir("/run/udev/links");
+ if (dir2)
+ cleanup_dir(dir2, 0, 2);
+
+ dir3 = opendir("/run/udev/tags");
+ if (dir3)
+ cleanup_dir(dir3, 0, 2);
+
+ dir4 = opendir("/run/udev/static_node-tags");
+ if (dir4)
+ cleanup_dir(dir4, 0, 2);
+
+ dir5 = opendir("/run/udev/watch");
+ if (dir5)
+ cleanup_dir(dir5, 0, 1);
+}
+
+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 *devlink, *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: {
+ const char *key, *value;
+
+ FOREACH_DEVICE_PROPERTY(device, key, value)
+ if (arg_export)
+ printf("%s%s='%s'\n", strempty(arg_export_prefix), key, value);
+ else
+ printf("%s=%s\n", key, value);
+ return 0;
+ }
+
+ case QUERY_ALL:
+ return print_record(device);
+ }
+
+ assert_not_reached("unknown query type");
+ return 0;
+}
+
+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"
+ " -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"
+ " -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"
+ , program_invocation_short_name);
+
+ return 0;
+}
+
+int info_main(int argc, char *argv[], void *userdata) {
+ _cleanup_strv_free_ char **devices = NULL;
+ _cleanup_free_ char *name = NULL;
+ int c, r;
+
+ static const struct option options[] = {
+ { "name", required_argument, NULL, 'n' },
+ { "path", required_argument, NULL, 'p' },
+ { "query", required_argument, NULL, 'q' },
+ { "attribute-walk", no_argument, NULL, 'a' },
+ { "cleanup-db", no_argument, NULL, 'c' },
+ { "export-db", no_argument, NULL, 'e' },
+ { "root", no_argument, NULL, 'r' },
+ { "device-id-of-file", required_argument, NULL, 'd' },
+ { "export", no_argument, NULL, 'x' },
+ { "export-prefix", required_argument, NULL, 'P' },
+ { "wait-for-initialization", optional_argument, NULL, 'w' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ ActionType action = ACTION_QUERY;
+ QueryType query = QUERY_ALL;
+
+ while ((c = getopt_long(argc, argv, "aced:n:p:q:rxP:w::Vh", options, NULL)) >= 0)
+ switch (c) {
+ 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 'e':
+ return export_devices();
+ 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 '?':
+ return -EINVAL;
+ default:
+ assert_not_reached("Unknown option");
+ }
+
+ 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);
+ }
+
+ r = strv_extend_strv(&devices, argv + optind, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build argument list: %m");
+
+ if (strv_isempty(devices))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "A device name or path is required");
+ if (action == ACTION_ATTRIBUTE_WALK && strv_length(devices) > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Only one device may be specified with -a/--attribute-walk");
+
+ char **p;
+ STRV_FOREACH(p, devices) {
+ _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+
+ r = find_device(*p, NULL, &device);
+ if (r == -EINVAL)
+ return log_error_errno(r, "Bad argument \"%s\", expected an absolute path in /dev/ or /sys or a unit name: %m", *p);
+ if (r < 0)
+ return log_error_errno(r, "Unknown device \"%s\": %m", *p);
+
+ if (arg_wait_for_initialization_timeout > 0) {
+ sd_device *d;
+
+ r = device_wait_for_initialization(
+ device,
+ NULL,
+ usec_add(now(CLOCK_MONOTONIC), 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
+ assert_not_reached("Unknown action");
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
diff --git a/src/udev/udevadm-monitor.c b/src/udev/udevadm-monitor.c
new file mode 100644
index 0000000..cae7f1b
--- /dev/null
+++ b/src/udev/udevadm-monitor.c
@@ -0,0 +1,254 @@
+/* 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) {
+ DeviceAction action = _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) 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) {
+ const char *key, *value;
+
+ 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");
+
+ (void) sd_device_monitor_set_receive_buffer_size(monitor, 128*1024*1024);
+
+ 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_event_source_set_description(sd_device_monitor_get_event_source(monitor),
+ sender == MONITOR_GROUP_UDEV ? "device-monitor-udev" : "device-monitor-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_allocated(&arg_subsystem_filter, NULL);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(arg_subsystem_filter, subsystem, devtype);
+ if (r < 0)
+ return r;
+
+ subsystem = devtype = NULL;
+ 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("Unknown option.");
+ }
+
+ 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_UNBLOCK, 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..2bd5853
--- /dev/null
+++ b/src/udev/udevadm-settle.c
@@ -0,0 +1,226 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2009 Canonical Ltd.
+ * Copyright © 2009 Scott James Remnant <scott@netsplit.com>
+ */
+
+#include <errno.h>
+#include <getopt.h>
+#include <poll.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "sd-bus.h"
+#include "sd-login.h"
+#include "sd-messages.h"
+
+#include "bus-util.h"
+#include "io-util.h"
+#include "libudev-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "udev-ctrl.h"
+#include "udevadm.h"
+#include "unit-def.h"
+#include "util.h"
+#include "virt.h"
+
+static usec_t arg_timeout = 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);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg);
+ break;
+ case 'E':
+ 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("Unknown option.");
+ }
+ }
+
+ 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,
+ "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;
+}
+
+int settle_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(udev_queue_unrefp) struct udev_queue *queue = NULL;
+ usec_t deadline;
+ int r, fd;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (running_in_chroot() > 0) {
+ log_info("Running in chroot, ignoring request.");
+ return 0;
+ }
+
+ deadline = now(CLOCK_MONOTONIC) + arg_timeout;
+
+ /* guarantee that the udev daemon isn't pre-processing */
+ if (getuid() == 0) {
+ _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *uctrl = NULL;
+
+ if (udev_ctrl_new(&uctrl) >= 0) {
+ r = udev_ctrl_send_ping(uctrl);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to connect to udev daemon: %m");
+ return 0;
+ }
+
+ r = udev_ctrl_wait(uctrl, MAX(5 * USEC_PER_SEC, arg_timeout));
+ if (r < 0)
+ return log_error_errno(r, "Failed to wait for daemon to reply: %m");
+ }
+ }
+
+ queue = udev_queue_new(NULL);
+ if (!queue)
+ return log_error_errno(errno, "Failed to get udev queue: %m");
+
+ fd = udev_queue_get_fd(queue);
+ if (fd < 0) {
+ log_debug_errno(fd, "Queue is empty, nothing to watch: %m");
+ return 0;
+ }
+
+ (void) emit_deprecation_warning();
+
+ for (;;) {
+ if (arg_exists && access(arg_exists, F_OK) >= 0)
+ return 0;
+
+ /* exit if queue is empty */
+ if (udev_queue_get_queue_is_empty(queue))
+ return 0;
+
+ if (now(CLOCK_MONOTONIC) >= deadline)
+ return -ETIMEDOUT;
+
+ /* wake up when queue becomes empty */
+ r = fd_wait_for_event(fd, POLLIN, MSEC_PER_SEC);
+ if (r < 0)
+ return r;
+ if (r & POLLIN) {
+ r = udev_queue_flush(queue);
+ if (r < 0)
+ return log_error_errno(r, "Failed to flush queue: %m");
+ }
+ }
+}
diff --git a/src/udev/udevadm-test-builtin.c b/src/udev/udevadm-test-builtin.c
new file mode 100644
index 0000000..8995e5c
--- /dev/null
+++ b/src/udev/udevadm-test-builtin.c
@@ -0,0 +1,97 @@
+/* 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 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"
+ "Commands:\n"
+ , program_invocation_short_name);
+
+ udev_builtin_list();
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ int c;
+
+ while ((c = getopt_long(argc, argv, "Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached("Unknown option");
+ }
+
+ 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),
+ "syspath missing.");
+
+ return 1;
+}
+
+int builtin_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ UdevBuiltinCommand cmd;
+ int r;
+
+ log_set_max_level(LOG_DEBUG);
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ udev_builtin_init();
+
+ cmd = udev_builtin_lookup(arg_command);
+ if (cmd < 0) {
+ log_error("Unknown command '%s'", arg_command);
+ r = -EINVAL;
+ goto finish;
+ }
+
+ r = find_device(arg_syspath, "/sys", &dev);
+ if (r < 0) {
+ log_error_errno(r, "Failed to open device '%s': %m", arg_syspath);
+ goto finish;
+ }
+
+ r = udev_builtin_run(dev, cmd, arg_command, true);
+ if (r < 0)
+ log_debug_errno(r, "Builtin command '%s' fails: %m", arg_command);
+
+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..a029622
--- /dev/null
+++ b/src/udev/udevadm-test.c
@@ -0,0 +1,162 @@
+/* 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 "libudev-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+#include "udev-event.h"
+#include "udevadm.h"
+
+static const char *arg_action = "add";
+static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY;
+static char arg_syspath[UTIL_PATH_SIZE] = {};
+
+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 c;
+
+ while ((c = getopt_long(argc, argv, "a:N:Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'a': {
+ DeviceAction a;
+
+ if (streq(optarg, "help")) {
+ dump_device_action_table();
+ return 0;
+ }
+
+ a = device_action_from_string(optarg);
+ if (a < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid action '%s'", optarg);
+
+ arg_action = optarg;
+ 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("Unknown option");
+ }
+
+ if (!argv[optind])
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "syspath parameter missing.");
+
+ /* add /sys if needed */
+ if (!path_startswith(argv[optind], "/sys"))
+ strscpyl(arg_syspath, sizeof(arg_syspath), "/sys", argv[optind], NULL);
+ else
+ strscpy(arg_syspath, sizeof(arg_syspath), argv[optind]);
+
+ 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, *key, *value;
+ sigset_t mask, sigmask_orig;
+ void *val;
+ int r;
+
+ log_set_max_level(LOG_DEBUG);
+
+ 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 = device_new_from_synthetic_event(&dev, arg_syspath, arg_action);
+ 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);
+
+ assert_se(sigfillset(&mask) >= 0);
+ assert_se(sigprocmask(SIG_SETMASK, &mask, &sigmask_orig) >= 0);
+
+ udev_event_execute_rules(event, 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[UTIL_PATH_SIZE];
+
+ (void) udev_event_apply_format(event, cmd, program, sizeof(program), false);
+ 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..5c74184
--- /dev/null
+++ b/src/udev/udevadm-trigger.c
@@ -0,0 +1,393 @@
+/* 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 "fd-util.h"
+#include "fileio.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "set.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 int exec_list(sd_device_enumerator *e, const char *action, Set **settle_set) {
+ sd_device *d;
+ int r, ret = 0;
+
+ FOREACH_DEVICE_AND_SUBSYSTEM(e, d) {
+ _cleanup_free_ char *filename = NULL;
+ const char *syspath;
+
+ if (sd_device_get_syspath(d, &syspath) < 0)
+ continue;
+
+ if (arg_verbose)
+ printf("%s\n", syspath);
+ if (arg_dry_run)
+ continue;
+
+ filename = path_join(syspath, "uevent");
+ if (!filename)
+ return log_oom();
+
+ r = write_string_file(filename, action, WRITE_STRING_FILE_DISABLE_BUFFER);
+ if (r < 0) {
+ bool ignore = IN_SET(r, -ENOENT, -ENODEV);
+
+ log_full_errno(ignore ? LOG_DEBUG : LOG_ERR, r,
+ "Failed to write '%s' to '%s'%s: %m",
+ action, filename, ignore ? ", ignoring" : "");
+ if (IN_SET(r, -EACCES, -EROFS))
+ /* Inovoked by unpriviledged user, or read only filesystem. Return earlier. */
+ return r;
+ if (ret == 0 && !ignore)
+ ret = r;
+ continue;
+ }
+
+ if (settle_set) {
+ r = set_put_strdup(settle_set, syspath);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ return ret;
+}
+
+static int device_monitor_handler(sd_device_monitor *m, sd_device *dev, void *userdata) {
+ _cleanup_free_ char *val = NULL;
+ Set *settle_set = userdata;
+ const char *syspath;
+
+ assert(dev);
+ assert(settle_set);
+
+ if (sd_device_get_syspath(dev, &syspath) < 0)
+ return 0;
+
+ if (arg_verbose)
+ printf("settle %s\n", syspath);
+
+ val = set_remove(settle_set, syspath);
+ if (!val)
+ log_debug("Got epoll event on syspath %s not present in syspath set", syspath);
+
+ if (set_isempty(settle_set))
+ 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"
+ " -t --type= Type of events to trigger\n"
+ " devices sysfs devices (default)\n"
+ " subsystems sysfs 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=KEY=VALUE Trigger devices with a matching property\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"
+ " -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"
+ , program_invocation_short_name);
+
+ return 0;
+}
+
+int trigger_main(int argc, char *argv[], void *userdata) {
+ enum {
+ ARG_NAME = 0x100,
+ ARG_PING,
+ };
+
+ static const struct option options[] = {
+ { "verbose", no_argument, NULL, 'v' },
+ { "dry-run", no_argument, NULL, 'n' },
+ { "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' },
+ { "settle", no_argument, NULL, 'w' },
+ { "wait-daemon", optional_argument, NULL, ARG_PING },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+ enum {
+ TYPE_DEVICES,
+ TYPE_SUBSYSTEMS,
+ } device_type = TYPE_DEVICES;
+ const char *action = "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_set = NULL;
+ usec_t ping_timeout_usec = 5 * USEC_PER_SEC;
+ bool settle = false, 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, "vnt: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 't':
+ if (streq(optarg, "devices"))
+ device_type = TYPE_DEVICES;
+ else if (streq(optarg, "subsystems"))
+ device_type = TYPE_SUBSYSTEMS;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type --type=%s", optarg);
+ break;
+ case 'c':
+ if (streq(optarg, "help")) {
+ dump_device_action_table();
+ return 0;
+ }
+ if (device_action_from_string(optarg) < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown action '%s'", optarg);
+
+ action = optarg;
+ 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':
+ 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 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached("Unknown option");
+ }
+ }
+
+ if (ping) {
+ _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *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 (settle) {
+ settle_set = set_new(&string_hash_ops_free);
+ if (!settle_set)
+ return log_oom();
+
+ 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_set);
+ 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;
+ default:
+ assert_not_reached("Unknown device type");
+ }
+
+ r = exec_list(e, action, settle ? &settle_set : NULL);
+ if (r < 0)
+ return r;
+
+ if (event && !set_isempty(settle_set)) {
+ 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..39d0c7e
--- /dev/null
+++ b/src/udev/udevadm-util.c
@@ -0,0 +1,95 @@
+/* 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_path(const char *path, sd_device **ret) {
+ if (path_startswith(path, "/sys/"))
+ return sd_device_new_from_syspath(ret, path);
+
+ if (path_startswith(path, "/dev/")) {
+ struct stat st;
+
+ if (stat(path, &st) < 0)
+ return -errno;
+
+ return device_new_from_stat_rdev(ret, &st);
+ }
+
+ return -EINVAL;
+}
+
+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 find_device_from_path(path, ret);
+ }
+
+ 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) {
+ _cleanup_free_ char *path = NULL;
+ int r;
+
+ assert(id);
+ assert(ret);
+
+ if (prefix) {
+ if (!path_startswith(id, prefix)) {
+ id = path = path_join(prefix, id);
+ if (!path)
+ return -ENOMEM;
+ }
+ } else {
+ /* In cases where the argument is generic (no prefix specified),
+ * check if the argument looks like a device unit name. */
+ r = find_device_from_unit(id, ret);
+ if (r >= 0)
+ return r;
+ }
+
+ return find_device_from_path(id, ret);
+}
diff --git a/src/udev/udevadm-util.h b/src/udev/udevadm-util.h
new file mode 100644
index 0000000..91587c5
--- /dev/null
+++ b/src/udev/udevadm-util.h
@@ -0,0 +1,6 @@
+/* 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);
diff --git a/src/udev/udevadm.c b/src/udev/udevadm.c
new file mode 100644
index 0000000..408e4a3
--- /dev/null
+++ b/src/udev/udevadm.c
@@ -0,0 +1,134 @@
+/* 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 "selinux-util.h"
+#include "string-util.h"
+#include "udevadm.h"
+#include "udevd.h"
+#include "udev-util.h"
+#include "verbs.h"
+#include "util.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" },
+ };
+
+ _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);
+
+ 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("Unhandled option");
+ }
+
+ 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 },
+ { "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 (strstr(program_invocation_short_name, "udevd"))
+ return run_udevd(argc, argv);
+
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ log_set_max_level_realm(LOG_REALM_SYSTEMD, log_get_max_level());
+
+ r = mac_selinux_init();
+ if (r < 0)
+ return r;
+
+ return udevadm_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/udevadm.h b/src/udev/udevadm.h
new file mode 100644
index 0000000..162bbb9
--- /dev/null
+++ b/src/udev/udevadm.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdio.h>
+
+#include "build.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);
+
+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..d24b8d4
--- /dev/null
+++ b/src/udev/udevd.c
@@ -0,0 +1,1960 @@
+/* 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 <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/epoll.h>
+#include <sys/file.h>
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/signalfd.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "sd-daemon.h"
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "build.h"
+#include "cgroup-util.h"
+#include "cpu-set-util.h"
+#include "dev-setup.h"
+#include "device-monitor-private.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "event-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "hashmap.h"
+#include "io-util.h"
+#include "libudev-device-internal.h"
+#include "limits-util.h"
+#include "list.h"
+#include "main-func.h"
+#include "mkdir.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "proc-cmdline.h"
+#include "process-util.h"
+#include "selinux-util.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "syslog-util.h"
+#include "udevd.h"
+#include "udev-builtin.h"
+#include "udev-ctrl.h"
+#include "udev-event.h"
+#include "udev-util.h"
+#include "udev-watch.h"
+#include "user-util.h"
+
+#define WORKER_NUM_MAX 2048U
+
+static bool arg_debug = false;
+static int arg_daemonize = false;
+static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY;
+static unsigned arg_children_max = 0;
+static usec_t arg_exec_delay_usec = 0;
+static usec_t arg_event_timeout_usec = 180 * USEC_PER_SEC;
+static int arg_timeout_signal = SIGKILL;
+static bool arg_blockdev_read_only = false;
+
+typedef struct Manager {
+ sd_event *event;
+ Hashmap *workers;
+ LIST_HEAD(struct event, events);
+ const char *cgroup;
+ pid_t pid; /* the process that originally allocated the manager object */
+
+ UdevRules *rules;
+ Hashmap *properties;
+
+ sd_netlink *rtnl;
+
+ sd_device_monitor *monitor;
+ struct udev_ctrl *ctrl;
+ int fd_inotify;
+ int worker_watch[2];
+
+ sd_event_source *inotify_event;
+ sd_event_source *kill_workers_event;
+
+ usec_t last_usec;
+
+ bool stop_exec_queue:1;
+ bool exit:1;
+} Manager;
+
+enum event_state {
+ EVENT_UNDEF,
+ EVENT_QUEUED,
+ EVENT_RUNNING,
+};
+
+struct event {
+ Manager *manager;
+ struct worker *worker;
+ enum event_state state;
+
+ sd_device *dev;
+ sd_device *dev_kernel; /* clone of originally received device */
+
+ uint64_t seqnum;
+ uint64_t delaying_seqnum;
+
+ sd_event_source *timeout_warning_event;
+ sd_event_source *timeout_event;
+
+ LIST_FIELDS(struct event, event);
+};
+
+static void event_queue_cleanup(Manager *manager, enum event_state type);
+
+enum worker_state {
+ WORKER_UNDEF,
+ WORKER_RUNNING,
+ WORKER_IDLE,
+ WORKER_KILLED,
+};
+
+struct worker {
+ Manager *manager;
+ pid_t pid;
+ sd_device_monitor *monitor;
+ enum worker_state state;
+ struct event *event;
+};
+
+/* passed from worker to main process */
+struct worker_message {
+};
+
+static void event_free(struct event *event) {
+ if (!event)
+ return;
+
+ assert(event->manager);
+
+ LIST_REMOVE(event, event->manager->events, event);
+ sd_device_unref(event->dev);
+ sd_device_unref(event->dev_kernel);
+
+ sd_event_source_unref(event->timeout_warning_event);
+ sd_event_source_unref(event->timeout_event);
+
+ if (event->worker)
+ event->worker->event = NULL;
+
+ /* only clean up the queue from the process that created it */
+ if (LIST_IS_EMPTY(event->manager->events) &&
+ event->manager->pid == getpid_cached())
+ if (unlink("/run/udev/queue") < 0)
+ log_warning_errno(errno, "Failed to unlink /run/udev/queue: %m");
+
+ free(event);
+}
+
+static void worker_free(struct worker *worker) {
+ if (!worker)
+ return;
+
+ assert(worker->manager);
+
+ hashmap_remove(worker->manager->workers, PID_TO_PTR(worker->pid));
+ sd_device_monitor_unref(worker->monitor);
+ event_free(worker->event);
+
+ free(worker);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct worker *, worker_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(worker_hash_op, void, trivial_hash_func, trivial_compare_func, struct worker, worker_free);
+
+static int worker_new(struct worker **ret, Manager *manager, sd_device_monitor *worker_monitor, pid_t pid) {
+ _cleanup_(worker_freep) struct 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(struct worker, 1);
+ if (!worker)
+ return -ENOMEM;
+
+ *worker = (struct worker) {
+ .manager = manager,
+ .monitor = sd_device_monitor_ref(worker_monitor),
+ .pid = pid,
+ };
+
+ r = hashmap_ensure_allocated(&manager->workers, &worker_hash_op);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(manager->workers, PID_TO_PTR(pid), worker);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(worker);
+
+ return 0;
+}
+
+static int on_event_timeout(sd_event_source *s, uint64_t usec, void *userdata) {
+ struct event *event = userdata;
+
+ assert(event);
+ assert(event->worker);
+
+ kill_and_sigcont(event->worker->pid, arg_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) {
+ struct event *event = userdata;
+
+ assert(event);
+ 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(struct worker *worker, struct event *event) {
+ 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;
+
+ e = worker->manager->event;
+
+ (void) sd_event_add_time_relative(e, &event->timeout_warning_event, CLOCK_MONOTONIC,
+ udev_warn_timeout(arg_event_timeout_usec), USEC_PER_SEC,
+ on_event_timeout_warning, event);
+
+ (void) sd_event_add_time_relative(e, &event->timeout_event, CLOCK_MONOTONIC,
+ arg_event_timeout_usec, USEC_PER_SEC,
+ on_event_timeout, event);
+}
+
+static void manager_clear_for_worker(Manager *manager) {
+ assert(manager);
+
+ manager->inotify_event = sd_event_source_unref(manager->inotify_event);
+ manager->kill_workers_event = sd_event_source_unref(manager->kill_workers_event);
+
+ manager->event = sd_event_unref(manager->event);
+
+ manager->workers = hashmap_free(manager->workers);
+ event_queue_cleanup(manager, EVENT_UNDEF);
+
+ manager->monitor = sd_device_monitor_unref(manager->monitor);
+ manager->ctrl = udev_ctrl_unref(manager->ctrl);
+
+ manager->worker_watch[READ_END] = safe_close(manager->worker_watch[READ_END]);
+}
+
+static void manager_free(Manager *manager) {
+ if (!manager)
+ return;
+
+ udev_builtin_exit();
+
+ if (manager->pid == getpid_cached())
+ udev_ctrl_cleanup(manager->ctrl);
+
+ manager_clear_for_worker(manager);
+
+ sd_netlink_unref(manager->rtnl);
+
+ hashmap_free_free_free(manager->properties);
+ udev_rules_free(manager->rules);
+
+ safe_close(manager->fd_inotify);
+ safe_close_pair(manager->worker_watch);
+
+ free(manager);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+static int worker_send_message(int fd) {
+ struct worker_message message = {};
+
+ return loop_write(fd, &message, sizeof(message), false);
+}
+
+static int worker_lock_block_device(sd_device *dev, int *ret_fd) {
+ _cleanup_close_ int fd = -1;
+ 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.
+ */
+
+ if (device_for_action(dev, DEVICE_ACTION_REMOVE))
+ 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");
+
+ if (STARTSWITH_SET(val, "dm-", "md", "drbd"))
+ return 0;
+
+ r = sd_device_get_devtype(dev, &val);
+ if (r < 0 && r != -ENOENT)
+ return log_device_debug_errno(dev, r, "Failed to get devtype: %m");
+ if (r >= 0 && streq(val, "partition")) {
+ r = sd_device_get_parent(dev, &dev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get parent device: %m");
+ }
+
+ r = sd_device_get_devname(dev, &val);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devname: %m");
+
+ fd = open(val, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK);
+ if (fd < 0) {
+ log_device_debug_errno(dev, errno, "Failed to open '%s', ignoring: %m", val);
+ return 0;
+ }
+
+ 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;
+}
+
+static int worker_mark_block_device_read_only(sd_device *dev) {
+ _cleanup_close_ int fd = -1;
+ const char *val;
+ int state = 1, r;
+
+ assert(dev);
+
+ if (!arg_blockdev_read_only)
+ return 0;
+
+ /* 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, DEVICE_ACTION_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;
+
+ r = sd_device_get_devname(dev, &val);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devname: %m");
+
+ fd = open(val, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK);
+ if (fd < 0)
+ return log_device_debug_errno(dev, errno, "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(Manager *manager, sd_device *dev) {
+ _cleanup_(udev_event_freep) UdevEvent *udev_event = NULL;
+ _cleanup_close_ int fd_lock = -1;
+ DeviceAction action;
+ uint64_t seqnum;
+ int r;
+
+ assert(manager);
+ assert(dev);
+
+ r = device_get_seqnum(dev, &seqnum);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get SEQNUM: %m");
+
+ r = device_get_action(dev, &action);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get ACTION: %m");
+
+ log_device_debug(dev, "Processing device (SEQNUM=%"PRIu64", ACTION=%s)",
+ seqnum, device_action_to_string(action));
+
+ udev_event = udev_event_new(dev, arg_exec_delay_usec, manager->rtnl);
+ if (!udev_event)
+ return -ENOMEM;
+
+ r = worker_lock_block_device(dev, &fd_lock);
+ if (r == -EAGAIN) {
+ /* So this is a block device and the device is locked currently via the BSD advisory locks —
+ * someone else is exclusively using it. This means we don't run our udev rules now, to not
+ * interfere. However we want to know when the device is unlocked again, and retrigger the
+ * device again then, so that the rules are run eventually. For that we use IN_CLOSE_WRITE
+ * inotify watches (which isn't exactly the same as waiting for the BSD locks to release, but
+ * not totally off, as long as unlock+close() is done together, as it usually is).
+ *
+ * (The user-facing side of this: https://systemd.io/BLOCK_DEVICE_LOCKING)
+ *
+ * There's a bit of a chicken and egg problem here for this however: inotify watching is
+ * supposed to be enabled via an option set via udev rules (OPTIONS+="watch"). If we skip the
+ * udev rules here however (as we just said we do), we would thus never see that specific
+ * udev rule, and thus never turn on inotify watching. But in order to catch up eventually
+ * and run them we we need the inotify watching: hence a classic chicken and egg problem.
+ *
+ * Our way out here: if we see the block device locked, unconditionally watch the device via
+ * inotify, regardless of any explicit request via OPTIONS+="watch". Thus, a device that is
+ * currently locked via the BSD file locks will be treated as if we ran a single udev rule
+ * only for it: the one that turns on inotify watching for it. If we eventually see the
+ * inotify IN_CLOSE_WRITE event, and then run the rules after all and we then realize that
+ * this wasn't actually requested (i.e. no OPTIONS+="watch" set) we'll simply turn off the
+ * watching again (see below). Effectively this means: inotify watching is now enabled either
+ * a) when the udev rules say so, or b) while the device is locked.
+ *
+ * Worst case scenario hence: in the (unlikely) case someone locked the device and we clash
+ * with that we might do inotify watching for a brief moment for a device where we actually
+ * weren't supposed to. But that shouldn't be too bad, in particular as BSD locks being taken
+ * on a block device is kinda an indication that the inotify logic is desired too, to some
+ * degree — they go hand-in-hand after all. */
+
+ log_device_debug(dev, "Block device is currently locked, installing watch to wait until the lock is released.");
+ (void) udev_watch_begin(dev);
+
+ /* Now the watch is installed, let's lock the device again, maybe in the meantime things changed */
+ r = worker_lock_block_device(dev, &fd_lock);
+ }
+ if (r < 0)
+ return r;
+
+ (void) worker_mark_block_device_read_only(dev);
+
+ /* apply rules, create node, symlinks */
+ r = udev_event_execute_rules(udev_event, arg_event_timeout_usec, arg_timeout_signal, manager->properties, manager->rules);
+ if (r < 0)
+ return r;
+
+ udev_event_execute_run(udev_event, arg_event_timeout_usec, arg_timeout_signal);
+
+ if (!manager->rtnl)
+ /* in case rtnl was initialized */
+ manager->rtnl = sd_netlink_ref(udev_event->rtnl);
+
+ /* apply/restore/end inotify watch */
+ if (udev_event->inotify_watch) {
+ (void) udev_watch_begin(dev);
+ r = device_update_db(dev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m");
+ } else
+ (void) udev_watch_end(dev);
+
+ log_device_debug(dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) processed",
+ seqnum, device_action_to_string(action));
+
+ return 0;
+}
+
+static int worker_device_monitor_handler(sd_device_monitor *monitor, sd_device *dev, void *userdata) {
+ Manager *manager = userdata;
+ int r;
+
+ assert(dev);
+ assert(manager);
+
+ r = worker_process_device(manager, dev);
+ if (r == -EAGAIN)
+ /* if we couldn't acquire the flock(), then proceed quietly */
+ log_device_debug_errno(dev, r, "Device currently locked, not processing.");
+ else {
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to process device, ignoring: %m");
+
+ /* send processed event back to libudev listeners */
+ r = device_monitor_send_device(monitor, NULL, dev);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to send device, ignoring: %m");
+ }
+
+ /* send udevd the result of the event execution */
+ r = worker_send_message(manager->worker_watch[WRITE_END]);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to send signal to main daemon, ignoring: %m");
+
+ return 1;
+}
+
+static int worker_main(Manager *_manager, sd_device_monitor *monitor, sd_device *first_device) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = first_device;
+ _cleanup_(manager_freep) Manager *manager = _manager;
+ int r;
+
+ assert(manager);
+ assert(monitor);
+ assert(dev);
+
+ assert_se(unsetenv("NOTIFY_SOCKET") == 0);
+
+ 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");
+
+ /* Clear unnecessary data in Manager object.*/
+ manager_clear_for_worker(manager);
+
+ r = sd_event_new(&manager->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ r = sd_event_add_signal(manager->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(monitor, manager->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event loop to device monitor: %m");
+
+ r = sd_device_monitor_start(monitor, worker_device_monitor_handler, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ (void) sd_event_source_set_description(sd_device_monitor_get_event_source(monitor), "worker-device-monitor");
+
+ /* Process first device */
+ (void) worker_device_monitor_handler(monitor, dev, manager);
+
+ r = sd_event_loop(manager->event);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
+
+static int worker_spawn(Manager *manager, struct event *event) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *worker_monitor = NULL;
+ struct 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;
+
+ /* 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(NULL, FORK_DEATHSIG, &pid);
+ if (r < 0) {
+ event->state = EVENT_QUEUED;
+ return log_error_errno(r, "Failed to fork() worker: %m");
+ }
+ if (r == 0) {
+ /* Worker process */
+ r = worker_main(manager, worker_monitor, sd_device_ref(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 void event_run(Manager *manager, struct event *event) {
+ static bool log_children_max_reached = true;
+ struct worker *worker;
+ int r;
+
+ assert(manager);
+ assert(event);
+
+ if (DEBUG_LOGGING) {
+ DeviceAction action;
+
+ r = device_get_action(event->dev, &action);
+ log_device_debug(event->dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) ready for processing",
+ event->seqnum, r >= 0 ? device_action_to_string(action) : "<unknown>");
+ }
+
+ 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;
+ }
+
+ if (hashmap_size(manager->workers) >= arg_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 && arg_children_max > 1) {
+ log_debug("Maximum number (%u) of children reached.", hashmap_size(manager->workers));
+ log_children_max_reached = false;
+ }
+ return;
+ }
+
+ /* Re-enable the debug message for the next batch of events */
+ log_children_max_reached = true;
+
+ /* fork with up-to-date SELinux label database, so the child inherits the up-to-date db
+ and, until the next SELinux policy changes, we safe further reloads in future children */
+ mac_selinux_maybe_reload();
+
+ /* start new worker and pass initial device */
+ worker_spawn(manager, event);
+}
+
+static int event_queue_insert(Manager *manager, sd_device *dev) {
+ _cleanup_(sd_device_unrefp) sd_device *clone = NULL;
+ struct event *event;
+ DeviceAction action;
+ uint64_t seqnum;
+ int r;
+
+ assert(manager);
+ assert(dev);
+
+ /* only one process can add events to the queue */
+ assert(manager->pid == getpid_cached());
+
+ /* We only accepts devices received by device monitor. */
+ r = device_get_seqnum(dev, &seqnum);
+ if (r < 0)
+ return r;
+
+ /* Refuse devices do not have ACTION property. */
+ r = device_get_action(dev, &action);
+ if (r < 0)
+ return r;
+
+ /* Save original device to restore the state on failures. */
+ r = device_shallow_clone(dev, &clone);
+ if (r < 0)
+ return r;
+
+ r = device_copy_properties(clone, dev);
+ if (r < 0)
+ return r;
+
+ event = new(struct event, 1);
+ if (!event)
+ return -ENOMEM;
+
+ *event = (struct event) {
+ .manager = manager,
+ .dev = sd_device_ref(dev),
+ .dev_kernel = TAKE_PTR(clone),
+ .seqnum = seqnum,
+ .state = EVENT_QUEUED,
+ };
+
+ if (LIST_IS_EMPTY(manager->events)) {
+ r = touch("/run/udev/queue");
+ if (r < 0)
+ log_warning_errno(r, "Failed to touch /run/udev/queue: %m");
+ }
+
+ LIST_APPEND(event, manager->events, event);
+
+ log_device_debug(dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) is queued",
+ seqnum, device_action_to_string(action));
+
+ return 0;
+}
+
+static void manager_kill_workers(Manager *manager) {
+ struct worker *worker;
+
+ assert(manager);
+
+ HASHMAP_FOREACH(worker, manager->workers) {
+ if (worker->state == WORKER_KILLED)
+ continue;
+
+ worker->state = WORKER_KILLED;
+ (void) kill(worker->pid, SIGTERM);
+ }
+}
+
+/* lookup event for identical, parent, child device */
+static int is_device_busy(Manager *manager, struct event *event) {
+ const char *subsystem, *devpath, *devpath_old = NULL;
+ dev_t devnum = makedev(0, 0);
+ struct event *loop_event;
+ size_t devpath_len;
+ int r, ifindex = 0;
+ bool is_block;
+
+ r = sd_device_get_subsystem(event->dev, &subsystem);
+ if (r < 0)
+ return r;
+
+ is_block = streq(subsystem, "block");
+
+ r = sd_device_get_devpath(event->dev, &devpath);
+ if (r < 0)
+ return r;
+
+ devpath_len = strlen(devpath);
+
+ r = sd_device_get_property_value(event->dev, "DEVPATH_OLD", &devpath_old);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ r = sd_device_get_devnum(event->dev, &devnum);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ r = sd_device_get_ifindex(event->dev, &ifindex);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ /* check if queue contains events we depend on */
+ LIST_FOREACH(event, loop_event, manager->events) {
+ size_t loop_devpath_len, common;
+ const char *loop_devpath;
+
+ /* we already found a later event, earlier cannot block us, no need to check again */
+ if (loop_event->seqnum < event->delaying_seqnum)
+ continue;
+
+ /* event we checked earlier still exists, no need to check again */
+ if (loop_event->seqnum == event->delaying_seqnum)
+ return true;
+
+ /* found ourself, no later event can block us */
+ if (loop_event->seqnum >= event->seqnum)
+ break;
+
+ /* check major/minor */
+ if (major(devnum) != 0) {
+ const char *s;
+ dev_t d;
+
+ if (sd_device_get_subsystem(loop_event->dev, &s) < 0)
+ continue;
+
+ if (sd_device_get_devnum(loop_event->dev, &d) >= 0 &&
+ devnum == d && is_block == streq(s, "block"))
+ goto set_delaying_seqnum;
+ }
+
+ /* check network device ifindex */
+ if (ifindex > 0) {
+ int i;
+
+ if (sd_device_get_ifindex(loop_event->dev, &i) >= 0 &&
+ ifindex == i)
+ goto set_delaying_seqnum;
+ }
+
+ if (sd_device_get_devpath(loop_event->dev, &loop_devpath) < 0)
+ continue;
+
+ /* check our old name */
+ if (devpath_old && streq(devpath_old, loop_devpath))
+ goto set_delaying_seqnum;
+
+ loop_devpath_len = strlen(loop_devpath);
+
+ /* compare devpath */
+ common = MIN(devpath_len, loop_devpath_len);
+
+ /* one devpath is contained in the other? */
+ if (!strneq(devpath, loop_devpath, common))
+ continue;
+
+ /* identical device event found */
+ if (devpath_len == loop_devpath_len)
+ goto set_delaying_seqnum;
+
+ /* parent device event found */
+ if (devpath[common] == '/')
+ goto set_delaying_seqnum;
+
+ /* child device event found */
+ if (loop_devpath[common] == '/')
+ goto set_delaying_seqnum;
+ }
+
+ return false;
+
+set_delaying_seqnum:
+ log_device_debug(event->dev, "SEQNUM=%" PRIu64 " blocked by SEQNUM=%" PRIu64,
+ event->seqnum, loop_event->seqnum);
+
+ event->delaying_seqnum = loop_event->seqnum;
+ return true;
+}
+
+static void manager_exit(Manager *manager) {
+ assert(manager);
+
+ manager->exit = true;
+
+ sd_notify(false,
+ "STOPPING=1\n"
+ "STATUS=Starting shutdown...");
+
+ /* close sources of new events and discard buffered events */
+ manager->ctrl = udev_ctrl_unref(manager->ctrl);
+
+ manager->inotify_event = sd_event_source_unref(manager->inotify_event);
+ manager->fd_inotify = safe_close(manager->fd_inotify);
+
+ manager->monitor = sd_device_monitor_unref(manager->monitor);
+
+ /* discard queued events and kill workers */
+ event_queue_cleanup(manager, EVENT_QUEUED);
+ manager_kill_workers(manager);
+}
+
+/* reload requested, HUP signal received, rules changed, builtin changed */
+static void manager_reload(Manager *manager) {
+
+ assert(manager);
+
+ sd_notify(false,
+ "RELOADING=1\n"
+ "STATUS=Flushing configuration...");
+
+ manager_kill_workers(manager);
+ manager->rules = udev_rules_free(manager->rules);
+ udev_builtin_exit();
+
+ sd_notifyf(false,
+ "READY=1\n"
+ "STATUS=Processing with %u children at max", arg_children_max);
+}
+
+static int on_kill_workers_event(sd_event_source *s, uint64_t usec, void *userdata) {
+ Manager *manager = userdata;
+
+ assert(manager);
+
+ log_debug("Cleanup idle workers");
+ manager_kill_workers(manager);
+
+ return 1;
+}
+
+static void event_queue_start(Manager *manager) {
+ struct event *event;
+ usec_t usec;
+ int r;
+
+ assert(manager);
+
+ if (LIST_IS_EMPTY(manager->events) ||
+ manager->exit || manager->stop_exec_queue)
+ return;
+
+ assert_se(sd_event_now(manager->event, CLOCK_MONOTONIC, &usec) >= 0);
+ /* check for changed config, every 3 seconds at most */
+ if (manager->last_usec == 0 ||
+ usec - manager->last_usec > 3 * USEC_PER_SEC) {
+ if (udev_rules_check_timestamp(manager->rules) ||
+ udev_builtin_validate())
+ manager_reload(manager);
+
+ manager->last_usec = usec;
+ }
+
+ 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");
+
+ udev_builtin_init();
+
+ if (!manager->rules) {
+ r = udev_rules_load(&manager->rules, arg_resolve_name_timing);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to read udev rules: %m");
+ return;
+ }
+ }
+
+ LIST_FOREACH(event, event, manager->events) {
+ if (event->state != EVENT_QUEUED)
+ continue;
+
+ /* do not start event if parent or child event is still running */
+ if (is_device_busy(manager, event) != 0)
+ continue;
+
+ event_run(manager, event);
+ }
+}
+
+static void event_queue_cleanup(Manager *manager, enum event_state match_type) {
+ struct event *event, *tmp;
+
+ LIST_FOREACH_SAFE(event, event, tmp, manager->events) {
+ if (match_type != EVENT_UNDEF && match_type != event->state)
+ continue;
+
+ event_free(event);
+ }
+}
+
+static int on_worker(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Manager *manager = userdata;
+
+ assert(manager);
+
+ for (;;) {
+ struct worker_message msg;
+ struct iovec iovec = {
+ .iov_base = &msg,
+ .iov_len = sizeof(msg),
+ };
+ 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;
+ struct 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(struct worker_message)) {
+ 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_KILLED)
+ worker->state = WORKER_IDLE;
+
+ /* worker returned */
+ event_free(worker->event);
+ }
+
+ /* we have free workers, try to schedule events */
+ event_queue_start(manager);
+
+ return 1;
+}
+
+static int on_uevent(sd_device_monitor *monitor, sd_device *dev, void *userdata) {
+ Manager *manager = userdata;
+ int r;
+
+ assert(manager);
+
+ 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;
+ }
+
+ /* we have fresh events, try to schedule them */
+ event_queue_start(manager);
+
+ return 1;
+}
+
+/* receive the udevd message from userspace */
+static int on_ctrl_msg(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, const union udev_ctrl_msg_value *value, void *userdata) {
+ Manager *manager = userdata;
+ int r;
+
+ assert(value);
+ assert(manager);
+
+ switch (type) {
+ case UDEV_CTRL_SET_LOG_LEVEL:
+ log_debug("Received udev control message (SET_LOG_LEVEL), setting log_level=%i", value->intval);
+ log_set_max_level_realm(LOG_REALM_UDEV, value->intval);
+ log_set_max_level_realm(LOG_REALM_SYSTEMD, value->intval);
+ manager_kill_workers(manager);
+ 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;
+ event_queue_start(manager);
+ break;
+ case UDEV_CTRL_RELOAD:
+ log_debug("Received udev control message (RELOAD)");
+ manager_reload(manager);
+ break;
+ case UDEV_CTRL_SET_ENV: {
+ _cleanup_free_ char *key = NULL, *val = NULL, *old_key = NULL, *old_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);
+
+ r = hashmap_put(manager->properties, key, NULL);
+ if (r < 0) {
+ log_oom();
+ return 1;
+ }
+ } 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);
+ 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);
+ arg_children_max = value->intval;
+
+ (void) sd_notifyf(false,
+ "READY=1\n"
+ "STATUS=Processing with %u children at max", arg_children_max);
+ 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, const char *syspath) {
+ const char *filename;
+ int r;
+
+ filename = strjoina(syspath, "/uevent");
+ log_device_debug(dev, "device is closed, synthesising 'change' on %s", syspath);
+ r = write_string_file(filename, "change", WRITE_STRING_FILE_DISABLE_BUFFER);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to write 'change' to %s: %m", filename);
+ return 0;
+}
+
+static int synthesize_change(sd_device *dev) {
+ const char *subsystem, *sysname, *devname, *syspath, *devtype;
+ int r;
+
+ r = sd_device_get_subsystem(dev, &subsystem);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devname(dev, &devname);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devtype(dev, &devtype);
+ if (r < 0)
+ return r;
+
+ if (streq_ptr("block", subsystem) &&
+ streq_ptr("disk", devtype) &&
+ !startswith(sysname, "dm-")) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ bool part_table_read = false, has_partitions = false;
+ sd_device *d;
+ int fd;
+
+ /*
+ * Try to re-read the partition table. This only succeeds if
+ * none of the devices is busy. The kernel returns 0 if no
+ * partition table is found, and we will not get an event for
+ * the disk.
+ */
+ fd = open(devname, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK);
+ if (fd >= 0) {
+ r = flock(fd, LOCK_EX|LOCK_NB);
+ if (r >= 0)
+ r = ioctl(fd, BLKRRPART, 0);
+
+ close(fd);
+ if (r >= 0)
+ part_table_read = true;
+ }
+
+ /* search for partitions */
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_allow_uninitialized(e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_parent(e, dev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_subsystem(e, "block", true);
+ if (r < 0)
+ return r;
+
+ FOREACH_DEVICE(e, d) {
+ const char *t;
+
+ if (sd_device_get_devtype(d, &t) < 0 ||
+ !streq("partition", t))
+ continue;
+
+ has_partitions = true;
+ break;
+ }
+
+ /*
+ * 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 && has_partitions)
+ return 0;
+
+ /*
+ * We have partitions but re-reading the partition table did not
+ * work, synthesize "change" for the disk and all partitions.
+ */
+ (void) synthesize_change_one(dev, syspath);
+
+ FOREACH_DEVICE(e, d) {
+ const char *t, *n, *s;
+
+ if (sd_device_get_devtype(d, &t) < 0 ||
+ !streq("partition", t))
+ continue;
+
+ if (sd_device_get_devname(d, &n) < 0 ||
+ sd_device_get_syspath(d, &s) < 0)
+ continue;
+
+ (void) synthesize_change_one(dev, s);
+ }
+
+ } else
+ (void) synthesize_change_one(dev, syspath);
+
+ return 0;
+}
+
+static int on_inotify(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Manager *manager = userdata;
+ union inotify_event_buffer buffer;
+ struct inotify_event *e;
+ ssize_t l;
+ int r;
+
+ assert(manager);
+
+ 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");
+
+ l = read(fd, &buffer, sizeof(buffer));
+ if (l < 0) {
+ if (IN_SET(errno, EAGAIN, EINTR))
+ return 1;
+
+ return log_error_errno(errno, "Failed to read inotify fd: %m");
+ }
+
+ FOREACH_INOTIFY_EVENT(e, buffer, l) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ const char *devnode;
+
+ if (udev_watch_lookup(e->wd, &dev) <= 0)
+ continue;
+
+ if (sd_device_get_devname(dev, &devnode) < 0)
+ continue;
+
+ log_device_debug(dev, "Inotify event: %x for %s", e->mask, devnode);
+ if (e->mask & IN_CLOSE_WRITE)
+ synthesize_change(dev);
+ else if (e->mask & IN_IGNORED)
+ udev_watch_end(dev);
+ }
+
+ return 1;
+}
+
+static int on_sigterm(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *manager = userdata;
+
+ assert(manager);
+
+ manager_exit(manager);
+
+ return 1;
+}
+
+static int on_sighup(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *manager = userdata;
+
+ assert(manager);
+
+ manager_reload(manager);
+
+ return 1;
+}
+
+static int on_sigchld(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *manager = userdata;
+ int r;
+
+ assert(manager);
+
+ for (;;) {
+ pid_t pid;
+ int status;
+ struct worker *worker;
+
+ pid = waitpid(-1, &status, WNOHANG);
+ if (pid <= 0)
+ break;
+
+ worker = hashmap_get(manager->workers, PID_TO_PTR(pid));
+ if (!worker) {
+ log_warning("Worker ["PID_FMT"] is unknown, ignoring", pid);
+ continue;
+ }
+
+ if (WIFEXITED(status)) {
+ if (WEXITSTATUS(status) == 0)
+ log_debug("Worker ["PID_FMT"] exited", pid);
+ else
+ log_warning("Worker ["PID_FMT"] exited with return code %i", pid, WEXITSTATUS(status));
+ } else if (WIFSIGNALED(status))
+ log_warning("Worker ["PID_FMT"] terminated by signal %i (%s)", pid, WTERMSIG(status), signal_to_string(WTERMSIG(status)));
+ else if (WIFSTOPPED(status)) {
+ log_info("Worker ["PID_FMT"] stopped", pid);
+ continue;
+ } else if (WIFCONTINUED(status)) {
+ log_info("Worker ["PID_FMT"] continued", pid);
+ continue;
+ } else
+ log_warning("Worker ["PID_FMT"] exit with status 0x%04x", pid, status);
+
+ if ((!WIFEXITED(status) || WEXITSTATUS(status) != 0) && worker->event) {
+ log_device_error(worker->event->dev, "Worker ["PID_FMT"] failed", pid);
+
+ /* delete state from disk */
+ device_delete_db(worker->event->dev);
+ device_tag_index(worker->event->dev, NULL, false);
+
+ if (manager->monitor) {
+ /* forward kernel event without amending it */
+ r = device_monitor_send_device(manager->monitor, NULL, worker->event->dev_kernel);
+ if (r < 0)
+ log_device_error_errno(worker->event->dev_kernel, r, "Failed to send back device to kernel: %m");
+ }
+ }
+
+ worker_free(worker);
+ }
+
+ /* we can start new workers, try to schedule events */
+ event_queue_start(manager);
+
+ /* Disable unnecessary cleanup event */
+ if (hashmap_isempty(manager->workers)) {
+ 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");
+ }
+
+ return 1;
+}
+
+static int on_post(sd_event_source *s, void *userdata) {
+ Manager *manager = userdata;
+
+ assert(manager);
+
+ if (!LIST_IS_EMPTY(manager->events))
+ return 1;
+
+ /* There are no pending events. Let's cleanup idle process. */
+
+ if (!hashmap_isempty(manager->workers)) {
+ /* There are idle workers */
+ (void) event_reset_time(manager->event, &manager->kill_workers_event, CLOCK_MONOTONIC,
+ now(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->exit)
+ return sd_event_exit(manager->event, 0);
+
+ if (manager->cgroup)
+ /* cleanup possible left-over processes in our cgroup */
+ (void) cg_kill(SYSTEMD_CGROUP_CONTROLLER, manager->cgroup, SIGKILL, CGROUP_IGNORE_SELF, NULL, NULL, NULL);
+
+ return 1;
+}
+
+static int listen_fds(int *ret_ctrl, int *ret_netlink) {
+ int ctrl_fd = -1, netlink_fd = -1;
+ 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_LOCAL, 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;
+}
+
+/*
+ * 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) {
+ 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, &arg_event_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, &arg_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, &arg_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)
+ arg_timeout_signal = r;
+
+ } else if (proc_cmdline_key_streq(key, "udev.blockdev_read_only")) {
+
+ if (!value)
+ arg_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
+ arg_blockdev_read_only = r;
+ }
+
+ if (arg_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[]) {
+ 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);
+
+ 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, &arg_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, &arg_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
+ arg_timeout_signal = r;
+
+ break;
+ case 't':
+ r = parse_sec(optarg, &arg_event_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
+ arg_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("Unhandled option");
+
+ }
+ }
+
+ return 1;
+}
+
+static int manager_new(Manager **ret, int fd_ctrl, int fd_uevent, const char *cgroup) {
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ int r;
+
+ assert(ret);
+
+ manager = new(Manager, 1);
+ if (!manager)
+ return log_oom();
+
+ *manager = (Manager) {
+ .fd_inotify = -1,
+ .worker_watch = { -1, -1 },
+ .cgroup = cgroup,
+ };
+
+ 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");
+
+ /* Bump receiver buffer, but only if we are not called via socket activation, as in that
+ * case systemd sets the receive buffer size for us, and the value in the .socket unit
+ * should take full effect. */
+ if (fd_uevent < 0) {
+ r = sd_device_monitor_set_receive_buffer_size(manager->monitor, 128 * 1024 * 1024);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set receive buffer size for device monitor, ignoring: %m");
+ }
+
+ r = device_monitor_enable_receiving(manager->monitor);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind netlink socket: %m");
+
+ *ret = TAKE_PTR(manager);
+
+ return 0;
+}
+
+static int main_loop(Manager *manager) {
+ int fd_worker, r;
+
+ manager->pid = getpid_cached();
+
+ /* unnamed socket from workers to the main daemon */
+ r = socketpair(AF_LOCAL, 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");
+
+ r = udev_watch_init();
+ if (r < 0)
+ return log_error_errno(r, "Failed to create inotify descriptor: %m");
+ manager->fd_inotify = r;
+
+ udev_watch_restore();
+
+ /* block and listen to all signals on signalfd */
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGHUP, SIGCHLD, -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_add_signal(manager->event, NULL, SIGCHLD, on_sigchld, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create SIGCHLD 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 device monitor: %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->fd_inotify, 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");
+
+ (void) sd_event_source_set_description(sd_device_monitor_get_event_source(manager->monitor), "device-monitor");
+
+ 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");
+
+ udev_builtin_init();
+
+ r = udev_rules_load(&manager->rules, arg_resolve_name_timing);
+ if (!manager->rules)
+ return log_error_errno(r, "Failed to read udev rules: %m");
+
+ r = udev_rules_apply_static_dev_perms(manager->rules);
+ if (r < 0)
+ log_error_errno(r, "Failed to apply permissions on static device nodes: %m");
+
+ (void) sd_notifyf(false,
+ "READY=1\n"
+ "STATUS=Processing with %u children at max", arg_children_max);
+
+ r = sd_event_loop(manager->event);
+ if (r < 0)
+ log_error_errno(r, "Event loop failed: %m");
+
+ sd_notify(false,
+ "STOPPING=1\n"
+ "STATUS=Shutting down...");
+ return r;
+}
+
+int run_udevd(int argc, char *argv[]) {
+ _cleanup_free_ char *cgroup = NULL;
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ int fd_ctrl = -1, fd_uevent = -1;
+ int r;
+
+ log_set_target(LOG_TARGET_AUTO);
+ log_open();
+ udev_parse_config_full(&arg_children_max, &arg_exec_delay_usec, &arg_event_timeout_usec, &arg_resolve_name_timing, &arg_timeout_signal);
+ log_parse_environment();
+ log_open(); /* Done again to update after reading configuration. */
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, 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);
+ }
+
+ log_set_max_level_realm(LOG_REALM_SYSTEMD, log_get_max_level());
+
+ r = must_be_root();
+ if (r < 0)
+ return r;
+
+ if (arg_children_max == 0) {
+ unsigned long cpu_limit, mem_limit, cpu_count = 1;
+
+ 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() / (128UL*1024*1024), 10U);
+
+ arg_children_max = MIN(cpu_limit, mem_limit);
+ arg_children_max = MIN(WORKER_NUM_MAX, arg_children_max);
+
+ log_debug("Set children_max to %u", arg_children_max);
+ }
+
+ /* set umask before creating any file/directory */
+ umask(022);
+
+ r = mac_selinux_init();
+ if (r < 0)
+ return r;
+
+ r = mkdir_errno_wrapper("/run/udev", 0755);
+ if (r < 0 && r != -EEXIST)
+ return log_error_errno(r, "Failed to create /run/udev: %m");
+
+ if (getppid() == 1 && sd_booted() > 0) {
+ /* Get our own cgroup, we regularly kill everything udev has left behind.
+ * We only do this on systemd systems, and only if we are directly spawned
+ * by PID1. Otherwise we are not guaranteed to have a dedicated cgroup. */
+ r = cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, 0, &cgroup);
+ if (r < 0) {
+ if (IN_SET(r, -ENOENT, -ENOMEDIUM))
+ log_debug_errno(r, "Dedicated cgroup not found: %m");
+ else
+ log_warning_errno(r, "Failed to get cgroup: %m");
+ }
+ }
+
+ r = listen_fds(&fd_ctrl, &fd_uevent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to listen on fds: %m");
+
+ r = manager_new(&manager, fd_ctrl, fd_uevent, cgroup);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create manager: %m");
+
+ if (arg_daemonize) {
+ pid_t pid;
+
+ log_info("Starting 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 main_loop(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..932446b
--- /dev/null
+++ b/src/udev/v4l_id/v4l_id.c
@@ -0,0 +1,93 @@
+/* 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 "fd-util.h"
+#include "util.h"
+
+int main(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+ _cleanup_close_ int fd = -1;
+ char *device;
+ struct v4l2_capability v2cap;
+ int c;
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+
+ switch (c) {
+ case 'h':
+ printf("%s [-h,--help] <device file>\n\n"
+ "Video4Linux device identification.\n\n"
+ " -h Print this message\n"
+ , program_invocation_short_name);
+ return 0;
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+
+ device = argv[optind];
+ if (!device)
+ return 2;
+
+ fd = open(device, O_RDONLY);
+ if (fd < 0)
+ return 3;
+
+ 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;
+}