diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 20:49:52 +0000 |
commit | 55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch) | |
tree | 33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/udev | |
parent | Initial commit. (diff) | |
download | systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip |
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/udev')
90 files changed, 24357 insertions, 0 deletions
diff --git a/src/udev/ata_id/ata_id.c b/src/udev/ata_id/ata_id.c new file mode 100644 index 0000000..4dd7e54 --- /dev/null +++ b/src/udev/ata_id/ata_id.c @@ -0,0 +1,635 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * ata_id - reads product/serial number from ATA drives + * + * Copyright © 2009-2010 David Zeuthen <zeuthen@gmail.com> + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <linux/bsg.h> +#include <linux/hdreg.h> +#include <scsi/scsi.h> +#include <scsi/scsi_ioctl.h> +#include <scsi/sg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "build.h" +#include "device-nodes.h" +#include "fd-util.h" +#include "log.h" +#include "main-func.h" +#include "memory-util.h" +#include "udev-util.h" +#include "unaligned.h" + +#define COMMAND_TIMEOUT_MSEC (30 * 1000) + +static bool arg_export = false; +static const char *arg_device = NULL; + +static int disk_scsi_inquiry_command( + int fd, + void *buf, + size_t buf_len) { + + uint8_t cdb[6] = { + /* INQUIRY, see SPC-4 section 6.4 */ + [0] = 0x12, /* OPERATION CODE: INQUIRY */ + [3] = (buf_len >> 8), /* ALLOCATION LENGTH */ + [4] = (buf_len & 0xff), + }; + uint8_t sense[32] = {}; + struct sg_io_v4 io_v4 = { + .guard = 'Q', + .protocol = BSG_PROTOCOL_SCSI, + .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD, + .request_len = sizeof(cdb), + .request = (uintptr_t) cdb, + .max_response_len = sizeof(sense), + .response = (uintptr_t) sense, + .din_xfer_len = buf_len, + .din_xferp = (uintptr_t) buf, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + if (ioctl(fd, SG_IO, &io_v4) != 0) { + if (errno != EINVAL) + return log_debug_errno(errno, "ioctl v4 failed: %m"); + + /* could be that the driver doesn't do version 4, try version 3 */ + struct sg_io_hdr io_hdr = { + .interface_id = 'S', + .cmdp = (unsigned char*) cdb, + .cmd_len = sizeof (cdb), + .dxferp = buf, + .dxfer_len = buf_len, + .sbp = sense, + .mx_sb_len = sizeof(sense), + .dxfer_direction = SG_DXFER_FROM_DEV, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + if (ioctl(fd, SG_IO, &io_hdr) != 0) + return log_debug_errno(errno, "ioctl v3 failed: %m"); + + /* even if the ioctl succeeds, we need to check the return value */ + if (io_hdr.status != 0 || + io_hdr.host_status != 0 || + io_hdr.driver_status != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v3 failed"); + + } else { + /* even if the ioctl succeeds, we need to check the return value */ + if (io_v4.device_status != 0 || + io_v4.transport_status != 0 || + io_v4.driver_status != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v4 failed"); + } + + return 0; +} + +static int disk_identify_command( + int fd, + void *buf, + size_t buf_len) { + + uint8_t cdb[12] = { + /* + * ATA Pass-Through 12 byte command, as described in + * + * T10 04-262r8 ATA Command Pass-Through + * + * from http://www.t10.org/ftp/t10/document.04/04-262r8.pdf + */ + [0] = 0xa1, /* OPERATION CODE: 12 byte pass through */ + [1] = 4 << 1, /* PROTOCOL: PIO Data-in */ + [2] = 0x2e, /* OFF_LINE=0, CK_COND=1, T_DIR=1, BYT_BLOK=1, T_LENGTH=2 */ + [3] = 0, /* FEATURES */ + [4] = 1, /* SECTORS */ + [5] = 0, /* LBA LOW */ + [6] = 0, /* LBA MID */ + [7] = 0, /* LBA HIGH */ + [8] = 0 & 0x4F, /* SELECT */ + [9] = 0xEC, /* Command: ATA IDENTIFY DEVICE */ + }; + uint8_t sense[32] = {}; + uint8_t *desc = sense + 8; + struct sg_io_v4 io_v4 = { + .guard = 'Q', + .protocol = BSG_PROTOCOL_SCSI, + .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD, + .request_len = sizeof(cdb), + .request = (uintptr_t) cdb, + .max_response_len = sizeof(sense), + .response = (uintptr_t) sense, + .din_xfer_len = buf_len, + .din_xferp = (uintptr_t) buf, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + if (ioctl(fd, SG_IO, &io_v4) != 0) { + if (errno != EINVAL) + return log_debug_errno(errno, "ioctl v4 failed: %m"); + + /* could be that the driver doesn't do version 4, try version 3 */ + struct sg_io_hdr io_hdr = { + .interface_id = 'S', + .cmdp = (unsigned char*) cdb, + .cmd_len = sizeof (cdb), + .dxferp = buf, + .dxfer_len = buf_len, + .sbp = sense, + .mx_sb_len = sizeof (sense), + .dxfer_direction = SG_DXFER_FROM_DEV, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + if (ioctl(fd, SG_IO, &io_hdr) != 0) + return log_debug_errno(errno, "ioctl v3 failed: %m"); + } else { + if (!((sense[0] & 0x7f) == 0x72 && desc[0] == 0x9 && desc[1] == 0x0c) && + !((sense[0] & 0x7f) == 0x70 && sense[12] == 0x00 && sense[13] == 0x1d)) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v4 failed: %m"); + } + + return 0; +} + +static int disk_identify_packet_device_command( + int fd, + void *buf, + size_t buf_len) { + + uint8_t cdb[16] = { + /* + * ATA Pass-Through 16 byte command, as described in + * + * T10 04-262r8 ATA Command Pass-Through + * + * from http://www.t10.org/ftp/t10/document.04/04-262r8.pdf + */ + [0] = 0x85, /* OPERATION CODE: 16 byte pass through */ + [1] = 4 << 1, /* PROTOCOL: PIO Data-in */ + [2] = 0x2e, /* OFF_LINE=0, CK_COND=1, T_DIR=1, BYT_BLOK=1, T_LENGTH=2 */ + [3] = 0, /* FEATURES */ + [4] = 0, /* FEATURES */ + [5] = 0, /* SECTORS */ + [6] = 1, /* SECTORS */ + [7] = 0, /* LBA LOW */ + [8] = 0, /* LBA LOW */ + [9] = 0, /* LBA MID */ + [10] = 0, /* LBA MID */ + [11] = 0, /* LBA HIGH */ + [12] = 0, /* LBA HIGH */ + [13] = 0, /* DEVICE */ + [14] = 0xA1, /* Command: ATA IDENTIFY PACKET DEVICE */ + [15] = 0, /* CONTROL */ + }; + uint8_t sense[32] = {}; + uint8_t *desc = sense + 8; + struct sg_io_v4 io_v4 = { + .guard = 'Q', + .protocol = BSG_PROTOCOL_SCSI, + .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD, + .request_len = sizeof (cdb), + .request = (uintptr_t) cdb, + .max_response_len = sizeof (sense), + .response = (uintptr_t) sense, + .din_xfer_len = buf_len, + .din_xferp = (uintptr_t) buf, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + if (ioctl(fd, SG_IO, &io_v4) != 0) { + if (errno != EINVAL) + return log_debug_errno(errno, "ioctl v4 failed: %m"); + + /* could be that the driver doesn't do version 4, try version 3 */ + struct sg_io_hdr io_hdr = { + .interface_id = 'S', + .cmdp = (unsigned char*) cdb, + .cmd_len = sizeof (cdb), + .dxferp = buf, + .dxfer_len = buf_len, + .sbp = sense, + .mx_sb_len = sizeof (sense), + .dxfer_direction = SG_DXFER_FROM_DEV, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + if (ioctl(fd, SG_IO, &io_hdr) != 0) + return log_debug_errno(errno, "ioctl v3 failed: %m"); + } else { + if ((sense[0] & 0x7f) != 0x72 || desc[0] != 0x9 || desc[1] != 0x0c) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v4 failed: %m"); + } + + return 0; +} + +/** + * disk_identify_get_string: + * @identify: A block of IDENTIFY data + * @offset_words: Offset of the string to get, in words. + * @dest: Destination buffer for the string. + * @dest_len: Length of destination buffer, in bytes. + * + * Copies the ATA string from @identify located at @offset_words into @dest. + */ +static void disk_identify_get_string( + uint8_t identify[512], + unsigned offset_words, + char *dest, + size_t dest_len) { + + unsigned c1; + unsigned c2; + + while (dest_len > 0) { + c1 = identify[offset_words * 2 + 1]; + c2 = identify[offset_words * 2]; + *dest = c1; + dest++; + *dest = c2; + dest++; + offset_words++; + dest_len -= 2; + } +} + +static void disk_identify_fixup_string( + uint8_t identify[512], + unsigned offset_words, + size_t len) { + assert(offset_words < 512/2); + disk_identify_get_string(identify, offset_words, + (char *) identify + offset_words * 2, len); +} + +static void disk_identify_fixup_uint16(uint8_t identify[512], unsigned offset_words) { + assert(offset_words < 512/2); + unaligned_write_ne16(identify + offset_words * 2, + unaligned_read_le16(identify + offset_words * 2)); +} + +/** + * disk_identify: + * @fd: File descriptor for the block device. + * @out_identify: Return location for IDENTIFY data. + * + * Sends the IDENTIFY DEVICE or IDENTIFY PACKET DEVICE command to the + * device represented by @fd. If successful, then the result will be + * copied into @out_identify. + * + * This routine is based on code from libatasmart, LGPL v2.1. + * + * Returns: 0 if the data was successfully obtained, otherwise + * non-zero with errno set. + */ +static int disk_identify(int fd, + uint8_t out_identify[512], + int *ret_peripheral_device_type) { + uint8_t inquiry_buf[36]; + int peripheral_device_type, r; + + /* init results */ + memzero(out_identify, 512); + + /* If we were to use ATA PASS_THROUGH (12) on an ATAPI device + * we could accidentally blank media. This is because MMC's BLANK + * command has the same op-code (0x61). + * + * To prevent this from happening we bail out if the device + * isn't a Direct Access Block Device, e.g. SCSI type 0x00 + * (CD/DVD devices are type 0x05). So we send a SCSI INQUIRY + * command first... libata is handling this via its SCSI + * emulation layer. + * + * This also ensures that we're actually dealing with a device + * that understands SCSI commands. + * + * (Yes, it is a bit perverse that we're tunneling the ATA + * command through SCSI and relying on the ATA driver + * emulating SCSI well-enough...) + * + * (See commit 160b069c25690bfb0c785994c7c3710289179107 for + * the original bug-fix and see http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=556635 + * for the original bug-report.) + */ + r = disk_scsi_inquiry_command(fd, inquiry_buf, sizeof inquiry_buf); + if (r < 0) + return r; + + /* SPC-4, section 6.4.2: Standard INQUIRY data */ + peripheral_device_type = inquiry_buf[0] & 0x1f; + if (peripheral_device_type == 0x05) { + r = disk_identify_packet_device_command(fd, out_identify, 512); + if (r < 0) + return r; + + } else { + if (!IN_SET(peripheral_device_type, 0x00, 0x14)) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Unsupported device type."); + + /* OK, now issue the IDENTIFY DEVICE command */ + r = disk_identify_command(fd, out_identify, 512); + if (r < 0) + return r; + } + + /* Check if IDENTIFY data is all NUL bytes - if so, bail */ + bool all_nul_bytes = true; + for (size_t n = 0; n < 512; n++) + if (out_identify[n] != '\0') { + all_nul_bytes = false; + break; + } + + if (all_nul_bytes) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "IDENTIFY data is all zeroes."); + + if (ret_peripheral_device_type) + *ret_peripheral_device_type = peripheral_device_type; + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "export", no_argument, NULL, 'x' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + {} + }; + int c; + + while ((c = getopt_long(argc, argv, "xh", options, NULL)) >= 0) + switch (c) { + case 'x': + arg_export = true; + break; + case 'h': + printf("%s [OPTIONS...] DEVICE\n\n" + " -x --export Print values as environment keys\n" + " -h --help Show this help text\n" + " --version Show package version\n", + program_invocation_short_name); + return 0; + case 'v': + return version(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (!argv[optind]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "DEVICE argument missing."); + + arg_device = argv[optind]; + return 1; +} + +static int run(int argc, char *argv[]) { + struct hd_driveid id; + union { + uint8_t byte[512]; + uint16_t wyde[256]; + } identify; + char model[41], model_enc[256], serial[21], revision[9]; + _cleanup_close_ int fd = -EBADF; + uint16_t word; + int r, peripheral_device_type = -1; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + fd = open(ASSERT_PTR(arg_device), O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY); + if (fd < 0) + return log_error_errno(errno, "Cannot open %s: %m", arg_device); + + if (disk_identify(fd, identify.byte, &peripheral_device_type) >= 0) { + /* + * fix up only the fields from the IDENTIFY data that we are going to + * use and copy it into the hd_driveid struct for convenience + */ + disk_identify_fixup_string(identify.byte, 10, 20); /* serial */ + disk_identify_fixup_string(identify.byte, 23, 8); /* fwrev */ + disk_identify_fixup_string(identify.byte, 27, 40); /* model */ + disk_identify_fixup_uint16(identify.byte, 0); /* configuration */ + disk_identify_fixup_uint16(identify.byte, 75); /* queue depth */ + disk_identify_fixup_uint16(identify.byte, 76); /* SATA capabilities */ + disk_identify_fixup_uint16(identify.byte, 82); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 83); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 84); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 85); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 86); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 87); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 89); /* time required for SECURITY ERASE UNIT */ + disk_identify_fixup_uint16(identify.byte, 90); /* time required for enhanced SECURITY ERASE UNIT */ + disk_identify_fixup_uint16(identify.byte, 91); /* current APM values */ + disk_identify_fixup_uint16(identify.byte, 94); /* current AAM value */ + disk_identify_fixup_uint16(identify.byte, 108); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 109); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 110); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 111); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 128); /* device lock function */ + disk_identify_fixup_uint16(identify.byte, 217); /* nominal media rotation rate */ + memcpy(&id, identify.byte, sizeof id); + } else { + /* If this fails, then try HDIO_GET_IDENTITY */ + if (ioctl(fd, HDIO_GET_IDENTITY, &id) != 0) + return log_debug_errno(errno, "%s: HDIO_GET_IDENTITY failed: %m", arg_device); + } + + memcpy(model, id.model, 40); + model[40] = '\0'; + encode_devnode_name(model, model_enc, sizeof(model_enc)); + udev_replace_whitespace((char *) id.model, model, 40); + udev_replace_chars(model, NULL); + udev_replace_whitespace((char *) id.serial_no, serial, 20); + udev_replace_chars(serial, NULL); + udev_replace_whitespace((char *) id.fw_rev, revision, 8); + udev_replace_chars(revision, NULL); + + if (arg_export) { + /* Set this to convey the disk speaks the ATA protocol */ + printf("ID_ATA=1\n"); + + if ((id.config >> 8) & 0x80) { + /* This is an ATAPI device */ + switch ((id.config >> 8) & 0x1f) { + case 0: + printf("ID_TYPE=cd\n"); + break; + case 1: + printf("ID_TYPE=tape\n"); + break; + case 5: + printf("ID_TYPE=cd\n"); + break; + case 7: + printf("ID_TYPE=optical\n"); + break; + default: + printf("ID_TYPE=generic\n"); + break; + } + } else + printf("ID_TYPE=disk\n"); + printf("ID_BUS=ata\n"); + printf("ID_MODEL=%s\n", model); + printf("ID_MODEL_ENC=%s\n", model_enc); + printf("ID_REVISION=%s\n", revision); + if (serial[0] != '\0') { + printf("ID_SERIAL=%s_%s\n", model, serial); + printf("ID_SERIAL_SHORT=%s\n", serial); + } else + printf("ID_SERIAL=%s\n", model); + + if (id.command_set_1 & (1<<5)) { + printf("ID_ATA_WRITE_CACHE=1\n"); + printf("ID_ATA_WRITE_CACHE_ENABLED=%d\n", (id.cfs_enable_1 & (1<<5)) ? 1 : 0); + } + if (id.command_set_1 & (1<<10)) { + printf("ID_ATA_FEATURE_SET_HPA=1\n"); + printf("ID_ATA_FEATURE_SET_HPA_ENABLED=%d\n", (id.cfs_enable_1 & (1<<10)) ? 1 : 0); + + /* + * TODO: use the READ NATIVE MAX ADDRESS command to get the native max address + * so it is easy to check whether the protected area is in use. + */ + } + if (id.command_set_1 & (1<<3)) { + printf("ID_ATA_FEATURE_SET_PM=1\n"); + printf("ID_ATA_FEATURE_SET_PM_ENABLED=%d\n", (id.cfs_enable_1 & (1<<3)) ? 1 : 0); + } + if (id.command_set_1 & (1<<1)) { + printf("ID_ATA_FEATURE_SET_SECURITY=1\n"); + printf("ID_ATA_FEATURE_SET_SECURITY_ENABLED=%d\n", (id.cfs_enable_1 & (1<<1)) ? 1 : 0); + printf("ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN=%d\n", id.trseuc * 2); + if ((id.cfs_enable_1 & (1<<1))) /* enabled */ { + if (id.dlf & (1<<8)) + printf("ID_ATA_FEATURE_SET_SECURITY_LEVEL=maximum\n"); + else + printf("ID_ATA_FEATURE_SET_SECURITY_LEVEL=high\n"); + } + if (id.dlf & (1<<5)) + printf("ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN=%d\n", id.trsEuc * 2); + if (id.dlf & (1<<4)) + printf("ID_ATA_FEATURE_SET_SECURITY_EXPIRE=1\n"); + if (id.dlf & (1<<3)) + printf("ID_ATA_FEATURE_SET_SECURITY_FROZEN=1\n"); + if (id.dlf & (1<<2)) + printf("ID_ATA_FEATURE_SET_SECURITY_LOCKED=1\n"); + } + if (id.command_set_1 & (1<<0)) { + printf("ID_ATA_FEATURE_SET_SMART=1\n"); + printf("ID_ATA_FEATURE_SET_SMART_ENABLED=%d\n", (id.cfs_enable_1 & (1<<0)) ? 1 : 0); + } + if (id.command_set_2 & (1<<9)) { + printf("ID_ATA_FEATURE_SET_AAM=1\n"); + printf("ID_ATA_FEATURE_SET_AAM_ENABLED=%d\n", (id.cfs_enable_2 & (1<<9)) ? 1 : 0); + printf("ID_ATA_FEATURE_SET_AAM_VENDOR_RECOMMENDED_VALUE=%d\n", id.acoustic >> 8); + printf("ID_ATA_FEATURE_SET_AAM_CURRENT_VALUE=%d\n", id.acoustic & 0xff); + } + if (id.command_set_2 & (1<<5)) { + printf("ID_ATA_FEATURE_SET_PUIS=1\n"); + printf("ID_ATA_FEATURE_SET_PUIS_ENABLED=%d\n", (id.cfs_enable_2 & (1<<5)) ? 1 : 0); + } + if (id.command_set_2 & (1<<3)) { + printf("ID_ATA_FEATURE_SET_APM=1\n"); + printf("ID_ATA_FEATURE_SET_APM_ENABLED=%d\n", (id.cfs_enable_2 & (1<<3)) ? 1 : 0); + if ((id.cfs_enable_2 & (1<<3))) + printf("ID_ATA_FEATURE_SET_APM_CURRENT_VALUE=%d\n", id.CurAPMvalues & 0xff); + } + if (id.command_set_2 & (1<<0)) + printf("ID_ATA_DOWNLOAD_MICROCODE=1\n"); + + /* + * Word 76 indicates the capabilities of a SATA device. A PATA device shall set + * word 76 to 0000h or FFFFh. If word 76 is set to 0000h or FFFFh, then + * the device does not claim compliance with the Serial ATA specification and words + * 76 through 79 are not valid and shall be ignored. + */ + + word = identify.wyde[76]; + if (!IN_SET(word, 0x0000, 0xffff)) { + printf("ID_ATA_SATA=1\n"); + /* + * If bit 2 of word 76 is set to one, then the device supports the Gen2 + * signaling rate of 3.0 Gb/s (see SATA 2.6). + * + * If bit 1 of word 76 is set to one, then the device supports the Gen1 + * signaling rate of 1.5 Gb/s (see SATA 2.6). + */ + if (word & (1<<2)) + printf("ID_ATA_SATA_SIGNAL_RATE_GEN2=1\n"); + if (word & (1<<1)) + printf("ID_ATA_SATA_SIGNAL_RATE_GEN1=1\n"); + } + + /* Word 217 indicates the nominal media rotation rate of the device */ + word = identify.wyde[217]; + if (word == 0x0001) + printf ("ID_ATA_ROTATION_RATE_RPM=0\n"); /* non-rotating e.g. SSD */ + else if (word >= 0x0401 && word <= 0xfffe) + printf ("ID_ATA_ROTATION_RATE_RPM=%d\n", word); + + /* + * Words 108-111 contain a mandatory World Wide Name (WWN) in the NAA IEEE Registered identifier + * format. Word 108 bits (15:12) shall contain 5h, indicating that the naming authority is IEEE. + * All other values are reserved. + */ + word = identify.wyde[108]; + if ((word & 0xf000) == 0x5000) { + uint64_t wwwn; + + wwwn = identify.wyde[108]; + wwwn <<= 16; + wwwn |= identify.wyde[109]; + wwwn <<= 16; + wwwn |= identify.wyde[110]; + wwwn <<= 16; + wwwn |= identify.wyde[111]; + printf("ID_WWN=0x%1$" PRIx64 "\n" + "ID_WWN_WITH_EXTENSION=0x%1$" PRIx64 "\n", + wwwn); + } + + /* from Linux's include/linux/ata.h */ + if (IN_SET(identify.wyde[0], 0x848a, 0x844a) || + (identify.wyde[83] & 0xc004) == 0x4004) + printf("ID_ATA_CFA=1\n"); + + if (peripheral_device_type >= 0) + printf("ID_ATA_PERIPHERAL_DEVICE_TYPE=%d\n", peripheral_device_type); + } else { + if (serial[0] != '\0') + printf("%s_%s\n", model, serial); + else + printf("%s\n", model); + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/cdrom_id/cdrom_id.c b/src/udev/cdrom_id/cdrom_id.c new file mode 100644 index 0000000..9285dd8 --- /dev/null +++ b/src/udev/cdrom_id/cdrom_id.c @@ -0,0 +1,1023 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * cdrom_id - optical drive and media information prober + */ + +#include <fcntl.h> +#include <getopt.h> +#include <linux/cdrom.h> +#include <scsi/sg.h> +#include <sys/ioctl.h> +#include <unistd.h> + +#include "build.h" +#include "fd-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "random-util.h" +#include "sort-util.h" +#include "string-table.h" +#include "string-util.h" +#include "udev-util.h" +#include "unaligned.h" + +static bool arg_eject = false; +static bool arg_lock = false; +static bool arg_unlock = false; +static const char *arg_node = NULL; + +typedef enum Feature { + FEATURE_RW_NONREMOVABLE = 0x01, + FEATURE_RW_REMOVABLE = 0x02, + + FEATURE_MO_SE = 0x03, /* sector erase */ + FEATURE_MO_WO = 0x04, /* write once */ + FEATURE_MO_AS = 0x05, /* advance storage */ + + FEATURE_CD_ROM = 0x08, + FEATURE_CD_R = 0x09, + FEATURE_CD_RW = 0x0a, + + FEATURE_DVD_ROM = 0x10, + FEATURE_DVD_R = 0x11, + FEATURE_DVD_RAM = 0x12, + FEATURE_DVD_RW_RO = 0x13, /* restricted overwrite mode */ + FEATURE_DVD_RW_SEQ = 0x14, /* sequential mode */ + FEATURE_DVD_R_DL_SEQ = 0x15, /* sequential recording */ + FEATURE_DVD_R_DL_JR = 0x16, /* jump recording */ + FEATURE_DVD_RW_DL = 0x17, + FEATURE_DVD_R_DDR = 0x18, /* download disc recording - dvd for css managed recording */ + FEATURE_DVD_PLUS_RW = 0x1a, + FEATURE_DVD_PLUS_R = 0x1b, + + FEATURE_DDCD_ROM = 0x20, + FEATURE_DDCD_R = 0x21, + FEATURE_DDCD_RW = 0x22, + + FEATURE_DVD_PLUS_RW_DL = 0x2a, + FEATURE_DVD_PLUS_R_DL = 0x2b, + + FEATURE_BD = 0x40, + FEATURE_BD_R_SRM = 0x41, /* sequential recording mode */ + FEATURE_BD_R_RRM = 0x42, /* random recording mode */ + FEATURE_BD_RE = 0x43, + + FEATURE_HDDVD = 0x50, + FEATURE_HDDVD_R = 0x51, + FEATURE_HDDVD_RAM = 0x52, + FEATURE_HDDVD_RW = 0x53, + FEATURE_HDDVD_R_DL = 0x58, + FEATURE_HDDVD_RW_DL = 0x5a, + + FEATURE_MRW, + FEATURE_MRW_W, + + _FEATURE_MAX, + _FEATURE_INVALID = -EINVAL, +} Feature; + +typedef enum MediaState { + MEDIA_STATE_BLANK = 0, + MEDIA_STATE_APPENDABLE = 1, + MEDIA_STATE_COMPLETE = 2, + MEDIA_STATE_OTHER = 3, + _MEDIA_STATE_MAX, + _MEDIA_STATE_INVALID = -EINVAL, +} MediaState; + +typedef struct Context { + int fd; + + Feature *drive_features; + size_t n_drive_feature; + + Feature media_feature; + bool has_media; + + MediaState media_state; + unsigned media_session_next; + unsigned media_session_count; + unsigned media_track_count; + unsigned media_track_count_data; + unsigned media_track_count_audio; + uint64_t media_session_last_offset; +} Context; + +#define CONTEXT_EMPTY { \ + .fd = -EBADF, \ + .media_feature = _FEATURE_INVALID, \ + .media_state = _MEDIA_STATE_INVALID, \ + } + +static void context_clear(Context *c) { + if (!c) + return; + + safe_close(c->fd); + free(c->drive_features); +} + +static bool drive_has_feature(const Context *c, Feature f) { + assert(c); + + for (size_t i = 0; i < c->n_drive_feature; i++) + if (c->drive_features[i] == f) + return true; + + return false; +} + +static int set_drive_feature(Context *c, Feature f) { + assert(c); + + if (drive_has_feature(c, f)) + return 0; + + if (!GREEDY_REALLOC(c->drive_features, c->n_drive_feature + 1)) + return -ENOMEM; + + c->drive_features[c->n_drive_feature++] = f; + return 1; +} + +#define ERRCODE(s) ((((s)[2] & 0x0F) << 16) | ((s)[12] << 8) | ((s)[13])) +#define SK(errcode) (((errcode) >> 16) & 0xFU) +#define ASC(errcode) (((errcode) >> 8) & 0xFFU) +#define ASCQ(errcode) ((errcode) & 0xFFU) +#define CHECK_CONDITION 0x01 + +static int log_scsi_debug_errno(int error, const char *msg) { + assert(error != 0); + + /* error < 0 means errno-style error, error > 0 means SCSI error */ + + if (error < 0) + return log_debug_errno(error, "Failed to %s: %m", msg); + + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "Failed to %s with SK=%X/ASC=%02X/ACQ=%02X", + msg, SK(error), ASC(error), ASCQ(error)); +} + +struct scsi_cmd { + struct cdrom_generic_command cgc; + union { + struct request_sense s; + unsigned char u[18]; + } _sense; + struct sg_io_hdr sg_io; +}; + +static void scsi_cmd_init(struct scsi_cmd *cmd) { + memzero(cmd, sizeof(struct scsi_cmd)); + cmd->cgc.quiet = 1; + cmd->cgc.sense = &cmd->_sense.s; + cmd->sg_io.interface_id = 'S'; + cmd->sg_io.mx_sb_len = sizeof(cmd->_sense); + cmd->sg_io.cmdp = cmd->cgc.cmd; + cmd->sg_io.sbp = cmd->_sense.u; + cmd->sg_io.flags = SG_FLAG_LUN_INHIBIT | SG_FLAG_DIRECT_IO; +} + +static void scsi_cmd_set(struct scsi_cmd *cmd, size_t i, unsigned char arg) { + cmd->sg_io.cmd_len = i + 1; + cmd->cgc.cmd[i] = arg; +} + +static int scsi_cmd_run(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize) { + int r; + + assert(cmd); + assert(fd >= 0); + assert(buf || bufsize == 0); + + /* Return 0 on success. On failure, return negative errno or positive error code. */ + + if (bufsize > 0) { + cmd->sg_io.dxferp = buf; + cmd->sg_io.dxfer_len = bufsize; + cmd->sg_io.dxfer_direction = SG_DXFER_FROM_DEV; + } else + cmd->sg_io.dxfer_direction = SG_DXFER_NONE; + + if (ioctl(fd, SG_IO, &cmd->sg_io) < 0) + return -errno; + + if ((cmd->sg_io.info & SG_INFO_OK_MASK) != SG_INFO_OK) { + if (cmd->sg_io.masked_status & CHECK_CONDITION) { + r = ERRCODE(cmd->_sense.u); + if (r != 0) + return r; + } + return -EIO; + } + + return 0; +} + +static int scsi_cmd_run_and_log(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize, const char *msg) { + int r; + + assert(msg); + + r = scsi_cmd_run(cmd, fd, buf, bufsize); + if (r != 0) + return log_scsi_debug_errno(r, msg); + + return 0; +} + +static int media_lock(int fd, bool lock) { + /* disable the kernel's lock logic */ + if (ioctl(fd, CDROM_CLEAR_OPTIONS, CDO_LOCK) < 0) + log_debug_errno(errno, "Failed to issue ioctl(CDROM_CLEAR_OPTIONS, CDO_LOCK), ignoring: %m"); + + if (ioctl(fd, CDROM_LOCKDOOR, lock ? 1 : 0) < 0) + return log_debug_errno(errno, "Failed to issue ioctl(CDROM_LOCKDOOR): %m"); + + return 0; +} + +static int media_eject(int fd) { + struct scsi_cmd sc; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_START_STOP_UNIT); + scsi_cmd_set(&sc, 4, 0x02); + scsi_cmd_set(&sc, 5, 0); + + return scsi_cmd_run_and_log(&sc, fd, NULL, 0, "start/stop unit"); +} + +static int cd_capability_compat(Context *c) { + int capability, r; + + assert(c); + + capability = ioctl(c->fd, CDROM_GET_CAPABILITY, NULL); + if (capability < 0) + return log_debug_errno(errno, "CDROM_GET_CAPABILITY failed"); + + if (capability & CDC_CD_R) { + r = set_drive_feature(c, FEATURE_CD_R); + if (r < 0) + return log_oom_debug(); + } + if (capability & CDC_CD_RW) { + r = set_drive_feature(c, FEATURE_CD_RW); + if (r < 0) + return log_oom_debug(); + } + if (capability & CDC_DVD) { + r = set_drive_feature(c, FEATURE_DVD_ROM); + if (r < 0) + return log_oom_debug(); + } + if (capability & CDC_DVD_R) { + r = set_drive_feature(c, FEATURE_DVD_R); + if (r < 0) + return log_oom_debug(); + } + if (capability & CDC_DVD_RAM) { + r = set_drive_feature(c, FEATURE_DVD_RAM); + if (r < 0) + return log_oom_debug(); + } + if (capability & CDC_MRW) { + r = set_drive_feature(c, FEATURE_MRW); + if (r < 0) + return log_oom_debug(); + } + if (capability & CDC_MRW_W) { + r = set_drive_feature(c, FEATURE_MRW_W); + if (r < 0) + return log_oom_debug(); + } + + return 0; +} + +static int cd_media_compat(Context *c) { + int r; + + assert(c); + + r = ioctl(c->fd, CDROM_DRIVE_STATUS, CDSL_CURRENT); + if (r < 0) + return log_debug_errno(errno, "ioctl(CDROM_DRIVE_STATUS) failed: %m"); + if (r != CDS_DISC_OK) + return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM), + "ioctl(CDROM_DRIVE_STATUS) → %d (%s), ignoring.", + r, + r == CDS_NO_INFO ? "no info" : + r == CDS_NO_DISC ? "no disc" : + r == CDS_TRAY_OPEN ? "tray open" : + r == CDS_DRIVE_NOT_READY ? "drive not ready" : + "unknown status"); + + c->has_media = true; + return 0; +} + +static int cd_inquiry(Context *c) { + struct scsi_cmd sc; + unsigned char inq[36]; + int r; + + assert(c); + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_INQUIRY); + scsi_cmd_set(&sc, 4, sizeof(inq)); + scsi_cmd_set(&sc, 5, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, inq, sizeof(inq), "inquire"); + if (r < 0) + return r; + + if ((inq[0] & 0x1F) != 5) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Not an MMC unit."); + + log_debug("INQUIRY: [%.8s][%.16s][%.4s]", inq + 8, inq + 16, inq + 32); + return 0; +} + +static int feature_profiles(Context *c, const unsigned char *profiles, size_t size) { + int r; + + assert(c); + + for (size_t i = 0; i + 4 <= size; i += 4) { + r = set_drive_feature(c, (Feature) unaligned_read_be16(&profiles[i])); + if (r < 0) + return log_oom_debug(); + } + + return 1; +} + +static int cd_profiles_old_mmc(Context *c) { + disc_information discinfo; + struct scsi_cmd sc; + size_t len; + int r; + + assert(c); + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO); + scsi_cmd_set(&sc, 8, sizeof(discinfo.disc_information_length)); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, (unsigned char *)&discinfo.disc_information_length, sizeof(discinfo.disc_information_length), "read disc information"); + if (r >= 0) { + /* Not all drives have the same disc_info length, so requeue + * packet with the length the drive tells us it can supply */ + len = be16toh(discinfo.disc_information_length) + sizeof(discinfo.disc_information_length); + if (len > sizeof(discinfo)) + len = sizeof(discinfo); + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO); + scsi_cmd_set(&sc, 8, len); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, (unsigned char *)&discinfo, len, "read disc information"); + } + if (r < 0) { + if (c->has_media) { + log_debug("No current profile, but disc is present; assuming CD-ROM."); + c->media_feature = FEATURE_CD_ROM; + c->media_track_count = 1; + c->media_track_count_data = 1; + return 1; + } else + return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM), + "no current profile, assuming no media."); + }; + + c->has_media = true; + + if (discinfo.erasable) + c->media_feature = FEATURE_CD_RW; + else if (discinfo.disc_status < 2 && drive_has_feature(c, FEATURE_CD_R)) + c->media_feature = FEATURE_CD_R; + else + c->media_feature = FEATURE_CD_ROM; + + return 0; +} + +static int cd_profiles(Context *c) { + struct scsi_cmd sc; + unsigned char features[65530]; + unsigned cur_profile; + size_t len; + int r; + + assert(c); + + /* First query the current profile */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_GET_CONFIGURATION); + scsi_cmd_set(&sc, 8, 8); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run(&sc, c->fd, features, 8); + if (r != 0) { + /* handle pre-MMC2 drives which do not support GET CONFIGURATION */ + if (r > 0 && SK(r) == 0x5 && IN_SET(ASC(r), 0x20, 0x24)) { + log_debug("Drive is pre-MMC2 and does not support 46h get configuration command; " + "trying to work around the problem."); + return cd_profiles_old_mmc(c); + } + + return log_scsi_debug_errno(r, "get configuration"); + } + + cur_profile = unaligned_read_be16(&features[6]); + if (cur_profile > 0) { + log_debug("current profile 0x%02x", cur_profile); + c->media_feature = (Feature) cur_profile; + c->has_media = true; + } else { + log_debug("no current profile, assuming no media"); + c->has_media = false; + } + + len = unaligned_read_be32(features); + log_debug("GET CONFIGURATION: size of features buffer %zu", len); + + if (len > sizeof(features)) { + log_debug("Cannot get features in a single query, truncating."); + len = sizeof(features); + } else if (len <= 8) + len = sizeof(features); + + /* Now get the full feature buffer */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_GET_CONFIGURATION); + scsi_cmd_set(&sc, 7, (len >> 8) & 0xff); + scsi_cmd_set(&sc, 8, len & 0xff); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, features, len, "get configuration"); + if (r < 0) + return r; + + /* parse the length once more, in case the drive decided to have other features suddenly :) */ + len = unaligned_read_be32(features); + log_debug("GET CONFIGURATION: size of features buffer %zu", len); + + if (len > sizeof(features)) { + log_debug("Cannot get features in a single query, truncating."); + len = sizeof(features); + } + + /* device features */ + for (size_t i = 8; i + 4 < len; i += 4 + features[i + 3]) { + unsigned feature; + + feature = unaligned_read_be16(&features[i]); + + switch (feature) { + case 0x00: + log_debug("GET CONFIGURATION: feature 'profiles', with %u entries", features[i + 3] / 4); + feature_profiles(c, features + i + 4, MIN(features[i + 3], len - i - 4)); + break; + default: + log_debug("GET CONFIGURATION: feature 0x%04x <ignored>, with 0x%02x bytes", feature, features[i + 3]); + break; + } + } + + return c->has_media; +} + +static const char * const media_state_table[_MEDIA_STATE_MAX] = { + [MEDIA_STATE_BLANK] = "blank", + [MEDIA_STATE_APPENDABLE] = "appendable", + [MEDIA_STATE_COMPLETE] = "complete", + [MEDIA_STATE_OTHER] = "other", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(media_state, MediaState); + +static int dvd_ram_media_update_state(Context *c) { + struct scsi_cmd sc; + unsigned char dvdstruct[8]; + unsigned char format[12]; + unsigned char len; + int r; + + assert(c); + + /* Return 1 if media state is determined. */ + + if (c->media_feature != FEATURE_DVD_RAM) + return 0; + + /* a write protected dvd-ram may report "complete" status */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_DVD_STRUCTURE); + scsi_cmd_set(&sc, 7, 0xC0); + scsi_cmd_set(&sc, 9, sizeof(dvdstruct)); + scsi_cmd_set(&sc, 11, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, dvdstruct, sizeof(dvdstruct), "read DVD structure"); + if (r < 0) + return r; + + if (dvdstruct[4] & 0x02) { + c->media_state = MEDIA_STATE_COMPLETE; + log_debug("Write-protected DVD-RAM media inserted"); + return 1; + } + + /* let's make sure we don't try to read unformatted media */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_FORMAT_CAPACITIES); + scsi_cmd_set(&sc, 8, sizeof(format)); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, format, sizeof(format), "read DVD format capacities"); + if (r < 0) + return r; + + len = format[3]; + if (len & 7 || len < 16) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid format capacities length."); + + switch (format[8] & 3) { + case 1: + /* This means that last format was interrupted or failed, blank dvd-ram discs are + * factory formatted. Take no action here as it takes quite a while to reformat a + * dvd-ram and it's not automatically started. */ + log_debug("Unformatted DVD-RAM media inserted."); + return 1; + + case 2: + log_debug("Formatted DVD-RAM media inserted."); + return 0; + + case 3: + c->has_media = false; + return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM), + "Format capacities returned no media."); + } + + return 0; +} + +static int dvd_media_update_state(Context *c) { + struct scsi_cmd sc; + unsigned char buffer[32 * 2048]; + int r; + + r = dvd_ram_media_update_state(c); + if (r != 0) + return r; + + /* Take a closer look at formatted media (unformatted DVD+RW + * has "blank" status", DVD-RAM was examined earlier) and check + * for ISO and UDF PVDs or a fs superblock presence and do it + * in one ioctl (we need just sectors 0 and 16) */ + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_10); + scsi_cmd_set(&sc, 5, 0); + scsi_cmd_set(&sc, 8, sizeof(buffer)/2048); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, buffer, sizeof(buffer), "read first 32 blocks"); + if (r < 0) { + c->has_media = false; + return r; + } + + /* if any non-zero data is found in sector 16 (iso and udf) or + * eventually 0 (fat32 boot sector, ext2 superblock, etc), disc + * is assumed non-blank */ + + for (size_t offset = 32768; offset < 32768 + 2048; offset++) + if (buffer[offset] != 0) { + log_debug("Data in block 16, assuming complete."); + return 0; + } + + for (size_t offset = 0; offset < 2048; offset++) + if (buffer[offset] != 0) { + log_debug("Data in block 0, assuming complete."); + return 0; + } + + log_debug("No data in blocks 0 or 16, assuming blank."); + c->media_state = MEDIA_STATE_BLANK; + return 0; +} + +static int cd_media_info(Context *c) { + struct scsi_cmd sc; + unsigned char header[32]; + MediaState state; + int r; + + assert(c); + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO); + scsi_cmd_set(&sc, 8, sizeof(header)); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read disc information"); + if (r < 0) + return r; + + c->has_media = true; + log_debug("disk type %02x", header[8]); + + state = (MediaState) (header[2] & 0x03); + log_debug("hardware reported media status: %s", strna(media_state_to_string(state))); + + /* exclude plain CDROM, some fake cdroms return 0 for "blank" media here */ + if (c->media_feature != FEATURE_CD_ROM) + c->media_state = state; + + /* fresh DVD-RW in restricted overwrite mode reports itself as + * "appendable"; change it to "blank" to make it consistent with what + * gets reported after blanking, and what userspace expects. */ + if (c->media_feature == FEATURE_DVD_RW_RO && state == MEDIA_STATE_APPENDABLE) + c->media_state = MEDIA_STATE_BLANK; + + /* DVD+RW discs (and DVD-RW in restricted mode) once formatted are + * always "complete", DVD-RAM are "other" or "complete" if the disc is + * write protected; we need to check the contents if it is blank */ + if (IN_SET(c->media_feature, FEATURE_DVD_RW_RO, FEATURE_DVD_PLUS_RW, FEATURE_DVD_PLUS_RW_DL, FEATURE_DVD_RAM) && + IN_SET(state, MEDIA_STATE_COMPLETE, MEDIA_STATE_OTHER)) { + r = dvd_media_update_state(c); + if (r < 0) + return r; + } + + /* "other" is e. g. DVD-RAM, can't append sessions there; DVDs in + * restricted overwrite mode can never append, only in sequential mode */ + if (c->media_feature != FEATURE_DVD_RW_RO && IN_SET(state, MEDIA_STATE_BLANK, MEDIA_STATE_APPENDABLE)) + c->media_session_next = header[10] << 8 | header[5]; + c->media_session_count = header[9] << 8 | header[4]; + c->media_track_count = header[11] << 8 | header[6]; + + return 0; +} + +static int cd_media_toc(Context *c) { + struct scsi_cmd sc; + unsigned char header[12]; + unsigned char toc[65536]; + unsigned num_tracks; + size_t len; + int r; + + assert(c); + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP); + scsi_cmd_set(&sc, 6, 1); + scsi_cmd_set(&sc, 8, sizeof(header)); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read TOC"); + if (r < 0) + return r; + + len = unaligned_read_be16(header) + 2; + log_debug("READ TOC: len: %zu, start track: %u, end track: %u", len, header[2], header[3]); + + if (len > sizeof(toc)) + return -1; + /* empty media has no tracks */ + if (len < 8) + return 0; + + /* 2: first track, 3: last track */ + num_tracks = header[3] - header[2] + 1; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP); + scsi_cmd_set(&sc, 6, header[2]); /* First Track/Session Number */ + scsi_cmd_set(&sc, 7, (len >> 8) & 0xff); + scsi_cmd_set(&sc, 8, len & 0xff); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, toc, len, "read TOC (tracks)"); + if (r < 0) + return r; + + /* Take care to not iterate beyond the last valid track as specified in + * the TOC, but also avoid going beyond the TOC length, just in case + * the last track number is invalidly large */ + for (size_t i = 4; i + 8 <= len && num_tracks > 0; i += 8, --num_tracks) { + bool is_data_track; + uint32_t block; + + is_data_track = (toc[i + 1] & 0x04) != 0; + block = unaligned_read_be32(&toc[i + 4]); + + log_debug("track=%u info=0x%x(%s) start_block=%"PRIu32, + toc[i + 2], toc[i + 1] & 0x0FU, is_data_track ? "data":"audio", block); + + if (is_data_track) + c->media_track_count_data++; + else + c->media_track_count_audio++; + } + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP); + scsi_cmd_set(&sc, 2, 1); /* Session Info */ + scsi_cmd_set(&sc, 8, sizeof(header)); + scsi_cmd_set(&sc, 9, 0); + r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read TOC (multi session)"); + if (r < 0) + return r; + + len = unaligned_read_be32(&header[8]); + log_debug("last track %u starts at block %zu", header[4+2], len); + c->media_session_last_offset = (uint64_t) len * 2048; + + return 0; +} + +static int open_drive(Context *c) { + int fd; + + assert(c); + assert(c->fd < 0); + + for (int cnt = 0;; cnt++) { + fd = open(arg_node, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY); + if (fd >= 0) + break; + if (++cnt >= 20 || errno != EBUSY) + return log_debug_errno(errno, "Unable to open '%s': %m", arg_node); + + (void) usleep_safe(100 * USEC_PER_MSEC + random_u64_range(100 * USEC_PER_MSEC)); + } + + log_debug("probing: '%s'", arg_node); + c->fd = fd; + return 0; +} + +typedef struct FeatureToString { + Feature feature; + const char *str; +} FeatureToString; + +static const FeatureToString feature_to_string[] = { + { .feature = FEATURE_RW_NONREMOVABLE, .str = "RW_NONREMOVABLE", }, + { .feature = FEATURE_RW_REMOVABLE, .str = "RW_REMOVABLE", }, + + { .feature = FEATURE_MO_SE, .str = "MO_SE", }, + { .feature = FEATURE_MO_WO, .str = "MO_WO", }, + { .feature = FEATURE_MO_AS, .str = "MO_AS", }, + + { .feature = FEATURE_CD_ROM, .str = "CD", }, + { .feature = FEATURE_CD_R, .str = "CD_R", }, + { .feature = FEATURE_CD_RW, .str = "CD_RW", }, + + { .feature = FEATURE_DVD_ROM, .str = "DVD", }, + { .feature = FEATURE_DVD_R, .str = "DVD_R", }, + { .feature = FEATURE_DVD_RAM, .str = "DVD_RAM", }, + { .feature = FEATURE_DVD_RW_RO, .str = "DVD_RW_RO", }, + { .feature = FEATURE_DVD_RW_SEQ, .str = "DVD_RW_SEQ", }, + { .feature = FEATURE_DVD_R_DL_SEQ, .str = "DVD_R_DL_SEQ", }, + { .feature = FEATURE_DVD_R_DL_JR, .str = "DVD_R_DL_JR", }, + { .feature = FEATURE_DVD_RW_DL, .str = "DVD_RW_DL", }, + { .feature = FEATURE_DVD_R_DDR, .str = "DVD_R_DDR", }, + { .feature = FEATURE_DVD_PLUS_RW, .str = "DVD_PLUS_RW", }, + { .feature = FEATURE_DVD_PLUS_R, .str = "DVD_PLUS_R", }, + + { .feature = FEATURE_DDCD_ROM, .str = "DDCD", }, + { .feature = FEATURE_DDCD_R, .str = "DDCD_R", }, + { .feature = FEATURE_DDCD_RW, .str = "DDCD_RW", }, + + { .feature = FEATURE_DVD_PLUS_RW_DL, .str = "DVD_PLUS_RW_DL", }, + { .feature = FEATURE_DVD_PLUS_R_DL, .str = "DVD_PLUS_R_DL", }, + + { .feature = FEATURE_BD, .str = "BD", }, + { .feature = FEATURE_BD_R_SRM, .str = "BD_R_SRM", }, + { .feature = FEATURE_BD_R_RRM, .str = "BD_R_RRM", }, + { .feature = FEATURE_BD_RE, .str = "BD_RE", }, + + { .feature = FEATURE_HDDVD, .str = "HDDVD", }, + { .feature = FEATURE_HDDVD_R, .str = "HDDVD_R", }, + { .feature = FEATURE_HDDVD_RAM, .str = "HDDVD_RAM", }, + { .feature = FEATURE_HDDVD_RW, .str = "HDDVD_RW", }, + { .feature = FEATURE_HDDVD_R_DL, .str = "HDDVD_R_DL", }, + { .feature = FEATURE_HDDVD_RW_DL, .str = "HDDVD_RW_DL", }, + + { .feature = FEATURE_MRW, .str = "MRW", }, + { .feature = FEATURE_MRW_W, .str = "MRW_W", }, +}; + +static int feature_to_string_compare_func(const FeatureToString *a, const FeatureToString *b) { + assert(a); + assert(b); + + return CMP(a->feature, b->feature); +} + +static void print_feature(Feature feature, const char *prefix) { + const FeatureToString *found, in = { + .feature = feature, + }; + + assert(prefix); + + found = typesafe_bsearch(&in, feature_to_string, ELEMENTSOF(feature_to_string), feature_to_string_compare_func); + if (!found) + return (void) log_debug("Unknown feature 0x%02x, ignoring.", (unsigned) feature); + + printf("%s_%s=1\n", prefix, found->str); +} + +static void print_properties(const Context *c) { + const char *state; + + assert(c); + + printf("ID_CDROM=1\n"); + for (size_t i = 0; i < c->n_drive_feature; i++) + print_feature(c->drive_features[i], "ID_CDROM"); + + if (drive_has_feature(c, FEATURE_MO_SE) || + drive_has_feature(c, FEATURE_MO_WO) || + drive_has_feature(c, FEATURE_MO_AS)) + printf("ID_CDROM_MO=1\n"); + + if (drive_has_feature(c, FEATURE_DVD_RW_RO) || + drive_has_feature(c, FEATURE_DVD_RW_SEQ)) + printf("ID_CDROM_DVD_RW=1\n"); + + if (drive_has_feature(c, FEATURE_DVD_R_DL_SEQ) || + drive_has_feature(c, FEATURE_DVD_R_DL_JR)) + printf("ID_CDROM_DVD_R_DL=1\n"); + + if (drive_has_feature(c, FEATURE_DVD_R_DDR)) + printf("ID_CDROM_DVD_R=1\n"); + + if (drive_has_feature(c, FEATURE_BD_R_SRM) || + drive_has_feature(c, FEATURE_BD_R_RRM)) + printf("ID_CDROM_BD_R=1\n"); + + if (c->has_media) { + printf("ID_CDROM_MEDIA=1\n"); + print_feature(c->media_feature, "ID_CDROM_MEDIA"); + + if (IN_SET(c->media_feature, FEATURE_MO_SE, FEATURE_MO_WO, FEATURE_MO_AS)) + printf("ID_CDROM_MEDIA_MO=1\n"); + + if (IN_SET(c->media_feature, FEATURE_DVD_RW_RO, FEATURE_DVD_RW_SEQ)) + printf("ID_CDROM_MEDIA_DVD_RW=1\n"); + + if (IN_SET(c->media_feature, FEATURE_DVD_R_DL_SEQ, FEATURE_DVD_R_DL_JR)) + printf("ID_CDROM_MEDIA_DVD_R_DL=1\n"); + + if (c->media_feature == FEATURE_DVD_R_DDR) + printf("ID_CDROM_MEDIA_DVD_R=1\n"); + + if (IN_SET(c->media_feature, FEATURE_BD_R_SRM, FEATURE_BD_R_RRM)) + printf("ID_CDROM_MEDIA_BD_R=1\n"); + } + + state = media_state_to_string(c->media_state); + if (state) + printf("ID_CDROM_MEDIA_STATE=%s\n", state); + if (c->media_session_next > 0) + printf("ID_CDROM_MEDIA_SESSION_NEXT=%u\n", c->media_session_next); + if (c->media_session_count > 0) + printf("ID_CDROM_MEDIA_SESSION_COUNT=%u\n", c->media_session_count); + if (c->media_session_count > 1 && c->media_session_last_offset > 0) + printf("ID_CDROM_MEDIA_SESSION_LAST_OFFSET=%" PRIu64 "\n", c->media_session_last_offset); + if (c->media_track_count > 0) + printf("ID_CDROM_MEDIA_TRACK_COUNT=%u\n", c->media_track_count); + if (c->media_track_count_audio > 0) + printf("ID_CDROM_MEDIA_TRACK_COUNT_AUDIO=%u\n", c->media_track_count_audio); + if (c->media_track_count_data > 0) + printf("ID_CDROM_MEDIA_TRACK_COUNT_DATA=%u\n", c->media_track_count_data); +} + +static int help(void) { + printf("%s [OPTIONS...] DEVICE\n\n" + " -l --lock-media Lock the media (to enable eject request events)\n" + " -u --unlock-media Unlock the media\n" + " -e --eject-media Eject the media\n" + " -d --debug Print debug messages to stderr\n" + " -h --help Show this help text\n" + " --version Show package version\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "lock-media", no_argument, NULL, 'l' }, + { "unlock-media", no_argument, NULL, 'u' }, + { "eject-media", no_argument, NULL, 'e' }, + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + {} + }; + int c; + + while ((c = getopt_long(argc, argv, "deluh", options, NULL)) >= 0) + switch (c) { + case 'l': + arg_lock = true; + break; + case 'u': + arg_unlock = true; + break; + case 'e': + arg_eject = true; + break; + case 'd': + log_set_target(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_DEBUG); + log_open(); + break; + case 'h': + return help(); + case 'v': + return version(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + arg_node = argv[optind]; + if (!arg_node) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No device specified."); + + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_(context_clear) Context c = CONTEXT_EMPTY; + int r; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = open_drive(&c); + if (r < 0) + return r; + + /* same data as original cdrom_id */ + r = cd_capability_compat(&c); + if (r < 0) + return r; + + /* check for media - don't bail if there's no media as we still need to + * to read profiles */ + (void) cd_media_compat(&c); + + /* check if drive talks MMC */ + if (cd_inquiry(&c) < 0) + goto work; + + r = cd_profiles(&c); /* read drive and possibly current profile */ + if (r > 0) { + /* at this point we are guaranteed to have media in the drive - find out more about it */ + + /* get session/track info */ + (void) cd_media_toc(&c); + + /* get writable media state */ + (void) cd_media_info(&c); + } + +work: + /* lock the media, so we enable eject button events */ + if (arg_lock && c.has_media) { + log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (lock)"); + (void) media_lock(c.fd, true); + } + + if (arg_unlock && c.has_media) { + log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)"); + (void) media_lock(c.fd, false); + } + + if (arg_eject) { + log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)"); + (void) media_lock(c.fd, false); + log_debug("START_STOP_UNIT (eject)"); + (void) media_eject(c.fd); + } + + print_properties(&c); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/dmi_memory_id/dmi_memory_id.c b/src/udev/dmi_memory_id/dmi_memory_id.c new file mode 100644 index 0000000..3f89cc7 --- /dev/null +++ b/src/udev/dmi_memory_id/dmi_memory_id.c @@ -0,0 +1,721 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * System Memory information + * + * Copyright (C) 2000-2002 Alan Cox <alan@redhat.com> + * Copyright (C) 2002-2020 Jean Delvare <jdelvare@suse.de> + * Copyright (C) 2020 Bastien Nocera <hadess@hadess.net> + * + * Unless specified otherwise, all references are aimed at the "System + * Management BIOS Reference Specification, Version 3.7.0" document, + * available from http://www.dmtf.org/standards/smbios. + * + * Note to contributors: + * Please reference every value you add or modify, especially if the + * information does not come from the above mentioned specification. + * + * Additional references: + * - Intel AP-485 revision 36 + * "Intel Processor Identification and the CPUID Instruction" + * http://www.intel.com/support/processors/sb/cs-009861.htm + * - DMTF Common Information Model + * CIM Schema version 2.19.1 + * http://www.dmtf.org/standards/cim/ + * - IPMI 2.0 revision 1.0 + * "Intelligent Platform Management Interface Specification" + * http://developer.intel.com/design/servers/ipmi/spec.htm + * - AMD publication #25481 revision 2.28 + * "CPUID Specification" + * http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/25481.pdf + * - BIOS Integrity Services Application Programming Interface version 1.0 + * http://www.intel.com/design/archives/wfm/downloads/bisspec.htm + * - DMTF DSP0239 version 1.1.0 + * "Management Component Transport Protocol (MCTP) IDs and Codes" + * http://www.dmtf.org/standards/pmci + * - "TPM Main, Part 2 TPM Structures" + * Specification version 1.2, level 2, revision 116 + * https://trustedcomputinggroup.org/tpm-main-specification/ + * - "PC Client Platform TPM Profile (PTP) Specification" + * Family "2.0", Level 00, Revision 00.43, January 26, 2015 + * https://trustedcomputinggroup.org/pc-client-platform-tpm-profile-ptp-specification/ + * - "RedFish Host Interface Specification" (DMTF DSP0270) + * https://www.dmtf.org/sites/default/files/DSP0270_1.0.1.pdf + */ + +#include <getopt.h> + +#include "alloc-util.h" +#include "build.h" +#include "fileio.h" +#include "main-func.h" +#include "string-util.h" +#include "udev-util.h" +#include "unaligned.h" + +#define SUPPORTED_SMBIOS_VER 0x030300 + +#define OUT_OF_SPEC_STR "<OUT OF SPEC>" + +#define SYS_FIRMWARE_DIR "/sys/firmware/dmi/tables" +#define SYS_ENTRY_FILE SYS_FIRMWARE_DIR "/smbios_entry_point" +#define SYS_TABLE_FILE SYS_FIRMWARE_DIR "/DMI" + +/* + * Per SMBIOS v2.8.0 and later, all structures assume a little-endian + * ordering convention. + */ +#define WORD(x) (unaligned_read_le16(x)) +#define DWORD(x) (unaligned_read_le32(x)) +#define QWORD(x) (unaligned_read_le64(x)) + +struct dmi_header { + uint8_t type; + uint8_t length; + uint16_t handle; + const uint8_t *data; +}; + +static const char *arg_source_file = NULL; + +static bool verify_checksum(const uint8_t *buf, size_t len) { + uint8_t sum = 0; + + for (size_t a = 0; a < len; a++) + sum += buf[a]; + return sum == 0; +} + +/* + * Type-independent Stuff + */ + +static const char *dmi_string(const struct dmi_header *dm, uint8_t s) { + const char *bp = (const char *) dm->data; + + if (s == 0) + return "Not Specified"; + + bp += dm->length; + for (;s > 1 && !isempty(bp); s--) + bp += strlen(bp) + 1; + + if (isempty(bp)) + return "<BAD INDEX>"; + + return bp; +} + +typedef enum { + MEMORY_SIZE_UNIT_BYTES, + MEMORY_SIZE_UNIT_KB +} MemorySizeUnit; + +static void dmi_print_memory_size( + const char *attr_prefix, const char *attr_suffix, + int slot_num, uint64_t code, MemorySizeUnit unit) { + if (unit == MEMORY_SIZE_UNIT_KB) + code <<= 10; + + if (slot_num >= 0) + printf("%s_%i_%s=%"PRIu64"\n", attr_prefix, slot_num, attr_suffix, code); + else + printf("%s_%s=%"PRIu64"\n", attr_prefix, attr_suffix, code); +} + +/* + * 7.17 Physical Memory Array (Type 16) + */ + +static void dmi_memory_array_location(uint8_t code) { + /* 7.17.1 */ + static const char *location[] = { + [0x01] = "Other", + [0x02] = "Unknown", + [0x03] = "System Board Or Motherboard", + [0x04] = "ISA Add-on Card", + [0x05] = "EISA Add-on Card", + [0x06] = "PCI Add-on Card", + [0x07] = "MCA Add-on Card", + [0x08] = "PCMCIA Add-on Card", + [0x09] = "Proprietary Add-on Card", + [0x0A] = "NuBus", + }; + static const char *location_0xA0[] = { + [0x00] = "PC-98/C20 Add-on Card", /* 0xA0 */ + [0x01] = "PC-98/C24 Add-on Card", /* 0xA1 */ + [0x02] = "PC-98/E Add-on Card", /* 0xA2 */ + [0x03] = "PC-98/Local Bus Add-on Card", /* 0xA3 */ + [0x04] = "CXL Add-on Card", /* 0xA4 */ + }; + const char *str = OUT_OF_SPEC_STR; + + if (code < ELEMENTSOF(location) && location[code]) + str = location[code]; + else if (code >= 0xA0 && code < (ELEMENTSOF(location_0xA0) + 0xA0)) + str = location_0xA0[code - 0xA0]; + + printf("MEMORY_ARRAY_LOCATION=%s\n", str); +} + +static void dmi_memory_array_ec_type(uint8_t code) { + /* 7.17.3 */ + static const char *type[] = { + [0x01] = "Other", + [0x02] = "Unknown", + [0x03] = "None", + [0x04] = "Parity", + [0x05] = "Single-bit ECC", + [0x06] = "Multi-bit ECC", + [0x07] = "CRC", + }; + + if (code != 0x03) /* Do not print "None". */ + printf("MEMORY_ARRAY_EC_TYPE=%s\n", + code < ELEMENTSOF(type) && type[code] ? type[code] : OUT_OF_SPEC_STR); +} + +/* + * 7.18 Memory Device (Type 17) + */ + +static void dmi_memory_device_string( + const char *attr_suffix, unsigned slot_num, + const struct dmi_header *h, uint8_t s) { + char *str; + + str = strdupa_safe(dmi_string(h, s)); + str = strstrip(str); + if (!isempty(str)) + printf("MEMORY_DEVICE_%u_%s=%s\n", slot_num, attr_suffix, str); +} + +static void dmi_memory_device_width( + const char *attr_suffix, + unsigned slot_num, uint16_t code) { + + /* If no memory module is present, width may be 0 */ + if (!IN_SET(code, 0, 0xFFFF)) + printf("MEMORY_DEVICE_%u_%s=%u\n", slot_num, attr_suffix, code); +} + +static void dmi_memory_device_size(unsigned slot_num, uint16_t code) { + if (code == 0) + return (void) printf("MEMORY_DEVICE_%u_PRESENT=0\n", slot_num); + if (code == 0xFFFF) + return; + + uint64_t s = code & 0x7FFF; + if (!(code & 0x8000)) + s <<= 10; + dmi_print_memory_size("MEMORY_DEVICE", "SIZE", slot_num, s, MEMORY_SIZE_UNIT_KB); +} + +static void dmi_memory_device_extended_size(unsigned slot_num, uint32_t code) { + uint64_t capacity = (uint64_t) code * 1024 * 1024; + + printf("MEMORY_DEVICE_%u_SIZE=%"PRIu64"\n", slot_num, capacity); +} + +static void dmi_memory_device_rank(unsigned slot_num, uint8_t code) { + code &= 0x0F; + if (code != 0) + printf("MEMORY_DEVICE_%u_RANK=%u\n", slot_num, code); +} + +static void dmi_memory_device_voltage_value( + const char *attr_suffix, + unsigned slot_num, uint16_t code) { + if (code == 0) + return; + if (code % 100 != 0) + printf("MEMORY_DEVICE_%u_%s=%g\n", slot_num, attr_suffix, (double)code / 1000); + else + printf("MEMORY_DEVICE_%u_%s=%.1g\n", slot_num, attr_suffix, (double)code / 1000); +} + +static void dmi_memory_device_form_factor(unsigned slot_num, uint8_t code) { + /* 7.18.1 */ + static const char *form_factor[] = { + [0x01] = "Other", + [0x02] = "Unknown", + [0x03] = "SIMM", + [0x04] = "SIP", + [0x05] = "Chip", + [0x06] = "DIP", + [0x07] = "ZIP", + [0x08] = "Proprietary Card", + [0x09] = "DIMM", + [0x0A] = "TSOP", + [0x0B] = "Row Of Chips", + [0x0C] = "RIMM", + [0x0D] = "SODIMM", + [0x0E] = "SRIMM", + [0x0F] = "FB-DIMM", + [0x10] = "Die", + }; + + printf("MEMORY_DEVICE_%u_FORM_FACTOR=%s\n", slot_num, + code < ELEMENTSOF(form_factor) && form_factor[code] ? form_factor[code] : OUT_OF_SPEC_STR); +} + +static void dmi_memory_device_set(unsigned slot_num, uint8_t code) { + if (code == 0xFF) + printf("MEMORY_DEVICE_%u_SET=%s\n", slot_num, "Unknown"); + else if (code != 0) + printf("MEMORY_DEVICE_%u_SET=%"PRIu8"\n", slot_num, code); +} + +static void dmi_memory_device_type(unsigned slot_num, uint8_t code) { + /* 7.18.2 */ + static const char *type[] = { + [0x01] = "Other", + [0x02] = "Unknown", + [0x03] = "DRAM", + [0x04] = "EDRAM", + [0x05] = "VRAM", + [0x06] = "SRAM", + [0x07] = "RAM", + [0x08] = "ROM", + [0x09] = "Flash", + [0x0A] = "EEPROM", + [0x0B] = "FEPROM", + [0x0C] = "EPROM", + [0x0D] = "CDRAM", + [0x0E] = "3DRAM", + [0x0F] = "SDRAM", + [0x10] = "SGRAM", + [0x11] = "RDRAM", + [0x12] = "DDR", + [0x13] = "DDR2", + [0x14] = "DDR2 FB-DIMM", + [0x15] = "Reserved", + [0x16] = "Reserved", + [0x17] = "Reserved", + [0x18] = "DDR3", + [0x19] = "FBD2", + [0x1A] = "DDR4", + [0x1B] = "LPDDR", + [0x1C] = "LPDDR2", + [0x1D] = "LPDDR3", + [0x1E] = "LPDDR4", + [0x1F] = "Logical non-volatile device", + [0x20] = "HBM", + [0x21] = "HBM2", + [0x22] = "DDR5", + [0x23] = "LPDDR5", + [0x24] = "HBM3", + }; + + printf("MEMORY_DEVICE_%u_TYPE=%s\n", slot_num, + code < ELEMENTSOF(type) && type[code] ? type[code] : OUT_OF_SPEC_STR); +} + +static void dmi_memory_device_type_detail(unsigned slot_num, uint16_t code) { + /* 7.18.3 */ + static const char *detail[] = { + [1] = "Other", + [2] = "Unknown", + [3] = "Fast-paged", + [4] = "Static Column", + [5] = "Pseudo-static", + [6] = "RAMBUS", + [7] = "Synchronous", + [8] = "CMOS", + [9] = "EDO", + [10] = "Window DRAM", + [11] = "Cache DRAM", + [12] = "Non-Volatile", + [13] = "Registered (Buffered)", + [14] = "Unbuffered (Unregistered)", + [15] = "LRDIMM", + }; + + if ((code & 0xFFFE) == 0) + printf("MEMORY_DEVICE_%u_TYPE_DETAIL=%s\n", slot_num, "None"); + else { + bool first_element = true; + + printf("MEMORY_DEVICE_%u_TYPE_DETAIL=", slot_num); + for (size_t i = 1; i < ELEMENTSOF(detail); i++) + if (code & (1 << i)) { + printf("%s%s", first_element ? "" : " ", detail[i]); + first_element = false; + } + printf("\n"); + } +} + +static void dmi_memory_device_speed( + const char *attr_suffix, + unsigned slot_num, uint16_t code) { + if (code != 0) + printf("MEMORY_DEVICE_%u_%s=%u\n", slot_num, attr_suffix, code); +} + +static void dmi_memory_device_technology(unsigned slot_num, uint8_t code) { + /* 7.18.6 */ + static const char * const technology[] = { + [0x01] = "Other", + [0x02] = "Unknown", + [0x03] = "DRAM", + [0x04] = "NVDIMM-N", + [0x05] = "NVDIMM-F", + [0x06] = "NVDIMM-P", + [0x07] = "Intel Optane persistent memory", + }; + + printf("MEMORY_DEVICE_%u_MEMORY_TECHNOLOGY=%s\n", slot_num, + code < ELEMENTSOF(technology) && technology[code] ? technology[code] : OUT_OF_SPEC_STR); +} + +static void dmi_memory_device_operating_mode_capability(unsigned slot_num, uint16_t code) { + /* 7.18.7 */ + static const char * const mode[] = { + [1] = "Other", + [2] = "Unknown", + [3] = "Volatile memory", + [4] = "Byte-accessible persistent memory", + [5] = "Block-accessible persistent memory", + }; + + if ((code & 0xFFFE) != 0) { + bool first_element = true; + + printf("MEMORY_DEVICE_%u_MEMORY_OPERATING_MODE_CAPABILITY=", slot_num); + for (size_t i = 1; i < ELEMENTSOF(mode); i++) + if (code & (1 << i)) { + printf("%s%s", first_element ? "" : " ", mode[i]); + first_element = false; + } + printf("\n"); + } +} + +static void dmi_memory_device_manufacturer_id( + const char *attr_suffix, + unsigned slot_num, uint16_t code) { + /* 7.18.8 */ + /* 7.18.10 */ + /* LSB is 7-bit Odd Parity number of continuation codes */ + if (code != 0) + printf("MEMORY_DEVICE_%u_%s=Bank %d, Hex 0x%02X\n", slot_num, attr_suffix, + (code & 0x7F) + 1, code >> 8); +} + +static void dmi_memory_device_product_id( + const char *attr_suffix, + unsigned slot_num, uint16_t code) { + /* 7.18.9 */ + /* 7.18.11 */ + if (code != 0) + printf("MEMORY_DEVICE_%u_%s=0x%04X\n", slot_num, attr_suffix, code); +} + +static void dmi_memory_device_size_detail( + const char *attr_suffix, + unsigned slot_num, uint64_t code) { + /* 7.18.12 */ + /* 7.18.13 */ + if (!IN_SET(code, 0x0LU, 0xFFFFFFFFFFFFFFFFLU)) + dmi_print_memory_size("MEMORY_DEVICE", attr_suffix, slot_num, code, MEMORY_SIZE_UNIT_BYTES); +} + +static void dmi_decode(const struct dmi_header *h, + unsigned *next_slot_num) { + const uint8_t *data = h->data; + unsigned slot_num; + + /* + * Note: DMI types 37 and 42 are untested + */ + switch (h->type) { + case 16: /* 7.17 Physical Memory Array */ + log_debug("Physical Memory Array"); + if (h->length < 0x0F) + break; + + if (data[0x05] != 0x03) /* 7.17.2, Use == "System Memory" */ + break; + + log_debug("Use: System Memory"); + dmi_memory_array_location(data[0x04]); + dmi_memory_array_ec_type(data[0x06]); + if (DWORD(data + 0x07) != 0x80000000) + dmi_print_memory_size("MEMORY_ARRAY", "MAX_CAPACITY", -1, DWORD(data + 0x07), MEMORY_SIZE_UNIT_KB); + else if (h->length >= 0x17) + dmi_print_memory_size("MEMORY_ARRAY", "MAX_CAPACITY", -1, QWORD(data + 0x0F), MEMORY_SIZE_UNIT_BYTES); + + break; + + case 17: /* 7.18 Memory Device */ + slot_num = *next_slot_num; + *next_slot_num = slot_num + 1; + + log_debug("Memory Device: %u", slot_num); + if (h->length < 0x15) + break; + + dmi_memory_device_width("TOTAL_WIDTH", slot_num, WORD(data + 0x08)); + dmi_memory_device_width("DATA_WIDTH", slot_num, WORD(data + 0x0A)); + if (h->length >= 0x20 && WORD(data + 0x0C) == 0x7FFF) + dmi_memory_device_extended_size(slot_num, DWORD(data + 0x1C)); + else + dmi_memory_device_size(slot_num, WORD(data + 0x0C)); + dmi_memory_device_form_factor(slot_num, data[0x0E]); + dmi_memory_device_set(slot_num, data[0x0F]); + dmi_memory_device_string("LOCATOR", slot_num, h, data[0x10]); + dmi_memory_device_string("BANK_LOCATOR", slot_num, h, data[0x11]); + dmi_memory_device_type(slot_num, data[0x12]); + dmi_memory_device_type_detail(slot_num, WORD(data + 0x13)); + if (h->length < 0x17) + break; + + dmi_memory_device_speed("SPEED_MTS", slot_num, WORD(data + 0x15)); + if (h->length < 0x1B) + break; + + dmi_memory_device_string("MANUFACTURER", slot_num, h, data[0x17]); + dmi_memory_device_string("SERIAL_NUMBER", slot_num, h, data[0x18]); + dmi_memory_device_string("ASSET_TAG", slot_num, h, data[0x19]); + dmi_memory_device_string("PART_NUMBER", slot_num, h, data[0x1A]); + if (h->length < 0x1C) + break; + + dmi_memory_device_rank(slot_num, data[0x1B]); + if (h->length < 0x22) + break; + + dmi_memory_device_speed("CONFIGURED_SPEED_MTS", slot_num, WORD(data + 0x20)); + if (h->length < 0x28) + break; + + dmi_memory_device_voltage_value("MINIMUM_VOLTAGE", slot_num, WORD(data + 0x22)); + dmi_memory_device_voltage_value("MAXIMUM_VOLTAGE", slot_num, WORD(data + 0x24)); + dmi_memory_device_voltage_value("CONFIGURED_VOLTAGE", slot_num, WORD(data + 0x26)); + if (h->length < 0x34) + break; + + dmi_memory_device_technology(slot_num, data[0x28]); + dmi_memory_device_operating_mode_capability(slot_num, WORD(data + 0x29)); + dmi_memory_device_string("FIRMWARE_VERSION", slot_num, h, data[0x2B]); + dmi_memory_device_manufacturer_id("MODULE_MANUFACTURER_ID", slot_num, WORD(data + 0x2C)); + dmi_memory_device_product_id("MODULE_PRODUCT_ID", slot_num, WORD(data + 0x2E)); + dmi_memory_device_manufacturer_id("MEMORY_SUBSYSTEM_CONTROLLER_MANUFACTURER_ID", + slot_num, WORD(data + 0x30)); + dmi_memory_device_product_id("MEMORY_SUBSYSTEM_CONTROLLER_PRODUCT_ID", + slot_num, WORD(data + 0x32)); + if (h->length < 0x3C) + break; + + dmi_memory_device_size_detail("NON_VOLATILE_SIZE", slot_num, QWORD(data + 0x34)); + if (h->length < 0x44) + break; + + dmi_memory_device_size_detail("VOLATILE_SIZE", slot_num, QWORD(data + 0x3C)); + if (h->length < 0x4C) + break; + + dmi_memory_device_size_detail("CACHE_SIZE", slot_num, QWORD(data + 0x44)); + if (h->length < 0x54) + break; + + dmi_memory_device_size_detail("LOGICAL_SIZE", slot_num, QWORD(data + 0x4C)); + + break; + } +} + +static void dmi_table_decode(const uint8_t *buf, size_t len, uint16_t num) { + const uint8_t *data = buf; + unsigned next_slot_num = 0; + + /* 4 is the length of an SMBIOS structure header */ + for (uint16_t i = 0; (i < num || num == 0) && data + 4 <= buf + len; i++) { + struct dmi_header h = (struct dmi_header) { + .type = data[0], + .length = data[1], + .handle = WORD(data + 2), + .data = data, + }; + bool display = !IN_SET(h.type, 126, 127); + const uint8_t *next; + + /* If a short entry is found (less than 4 bytes), not only it + * is invalid, but we cannot reliably locate the next entry. + * Better stop at this point, and let the user know their + * table is broken. */ + if (h.length < 4) + break; + + /* In quiet mode, stop decoding at end of table marker */ + if (h.type == 127) + break; + + /* Look for the next handle */ + next = data + h.length; + while ((size_t)(next - buf + 1) < len && (next[0] != 0 || next[1] != 0)) + next++; + next += 2; + + /* Make sure the whole structure fits in the table */ + if ((size_t)(next - buf) > len) + break; + + if (display) + dmi_decode(&h, &next_slot_num); + + data = next; + } + if (next_slot_num > 0) + printf("MEMORY_ARRAY_NUM_DEVICES=%u\n", next_slot_num); +} + +static int dmi_table(int64_t base, uint32_t len, uint16_t num, const char *devmem, bool no_file_offset) { + _cleanup_free_ uint8_t *buf = NULL; + size_t size; + int r; + + /* + * When reading from sysfs or from a dump file, the file may be + * shorter than announced. For SMBIOS v3 this is expected, as we + * only know the maximum table size, not the actual table size. + * For older implementations (and for SMBIOS v3 too), this + * would be the result of the kernel truncating the table on + * parse error. + */ + r = read_full_file_full(AT_FDCWD, devmem, no_file_offset ? 0 : base, len, + 0, NULL, (char **) &buf, &size); + if (r < 0) + return log_error_errno(r, "Failed to read table: %m"); + + dmi_table_decode(buf, size, num); + + return 0; +} + +/* Same thing for SMBIOS3 entry points */ +static int smbios3_decode(const uint8_t *buf, const char *devmem, bool no_file_offset) { + uint64_t offset; + + /* Don't let checksum run beyond the buffer */ + if (buf[0x06] > 0x20) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Entry point length too large (%"PRIu8" bytes, expected %u).", + buf[0x06], 0x18U); + + if (!verify_checksum(buf, buf[0x06])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to verify checksum."); + + offset = QWORD(buf + 0x10); + +#if __SIZEOF_SIZE_T__ != 8 + if (!no_file_offset && (offset >> 32) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "64-bit addresses not supported on 32-bit systems."); +#endif + + return dmi_table(offset, DWORD(buf + 0x0C), 0, devmem, no_file_offset); +} + +static int smbios_decode(const uint8_t *buf, const char *devmem, bool no_file_offset) { + /* Don't let checksum run beyond the buffer */ + if (buf[0x05] > 0x20) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Entry point length too large (%"PRIu8" bytes, expected %u).", + buf[0x05], 0x1FU); + + if (!verify_checksum(buf, buf[0x05]) + || memcmp(buf + 0x10, "_DMI_", 5) != 0 + || !verify_checksum(buf + 0x10, 0x0F)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to verify checksum."); + + return dmi_table(DWORD(buf + 0x18), WORD(buf + 0x16), WORD(buf + 0x1C), + devmem, no_file_offset); +} + +static int legacy_decode(const uint8_t *buf, const char *devmem, bool no_file_offset) { + if (!verify_checksum(buf, 0x0F)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to verify checksum."); + + return dmi_table(DWORD(buf + 0x08), WORD(buf + 0x06), WORD(buf + 0x0C), + devmem, no_file_offset); +} + +static int help(void) { + printf("%s [OPTIONS...]\n\n" + " -F --from-dump FILE Read DMI information from a binary file\n" + " -h --help Show this help text\n" + " --version Show package version\n", + program_invocation_short_name); + return 0; +} + +static int parse_argv(int argc, char * const *argv) { + static const struct option options[] = { + { "from-dump", required_argument, NULL, 'F' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + {} + }; + int c; + + while ((c = getopt_long(argc, argv, "F:hV", options, NULL)) >= 0) + switch (c) { + case 'F': + arg_source_file = optarg; + break; + case 'V': + return version(); + case 'h': + return help(); + case '?': + return -EINVAL; + case 'v': + return version(); + default: + assert_not_reached(); + } + + return 1; +} + +static int run(int argc, char* const* argv) { + _cleanup_free_ uint8_t *buf = NULL; + bool no_file_offset = false; + size_t size; + int r; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + /* Read from dump if so instructed */ + r = read_full_file_full(AT_FDCWD, + arg_source_file ?: SYS_ENTRY_FILE, + 0, 0x20, 0, NULL, (char **) &buf, &size); + if (r < 0) + return log_full_errno(!arg_source_file && r == -ENOENT ? LOG_DEBUG : LOG_ERR, + r, "Reading \"%s\" failed: %m", + arg_source_file ?: SYS_ENTRY_FILE); + + if (!arg_source_file) { + arg_source_file = SYS_TABLE_FILE; + no_file_offset = true; + } + + if (size >= 24 && memory_startswith(buf, size, "_SM3_")) + return smbios3_decode(buf, arg_source_file, no_file_offset); + if (size >= 31 && memory_startswith(buf, size, "_SM_")) + return smbios_decode(buf, arg_source_file, no_file_offset); + if (size >= 15 && memory_startswith(buf, size, "_DMI_")) + return legacy_decode(buf, arg_source_file, no_file_offset); + + return -EINVAL; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/fido_id/fido_id.c b/src/udev/fido_id/fido_id.c new file mode 100644 index 0000000..e01f37d --- /dev/null +++ b/src/udev/fido_id/fido_id.c @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on the usage declared in their report + * descriptor and outputs suitable environment variables. + * + * Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' + */ + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <linux/hid.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <unistd.h> + +#include "build.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "fido_id_desc.h" +#include "log.h" +#include "macro.h" +#include "main-func.h" +#include "path-util.h" +#include "string-util.h" +#include "udev-util.h" + +static const char *arg_device = NULL; + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + {} + }; + int c; + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + case 'h': + printf("%s [OPTIONS...] SYSFS_PATH\n\n" + " -h --help Show this help text\n" + " --version Show package version\n", + program_invocation_short_name); + return 0; + case 'v': + return version(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (argc > 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error: unexpected argument."); + + arg_device = argv[optind]; + return 1; +} + +static int run(int argc, char **argv) { + _cleanup_(sd_device_unrefp) struct sd_device *device = NULL; + _cleanup_free_ char *desc_path = NULL; + _cleanup_close_ int fd = -EBADF; + struct sd_device *hid_device; + const char *sys_path; + uint8_t desc[HID_MAX_DESCRIPTOR_SIZE]; + ssize_t desc_len; + int r; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (arg_device) { + r = sd_device_new_from_syspath(&device, arg_device); + if (r < 0) + return log_error_errno(r, "Failed to get device from syspath %s: %m", arg_device); + } else { + r = device_new_from_strv(&device, environ); + if (r < 0) + return log_error_errno(r, "Failed to get current device from environment: %m"); + } + + r = sd_device_get_parent(device, &hid_device); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get parent HID device: %m"); + + r = sd_device_get_syspath(hid_device, &sys_path); + if (r < 0) + return log_device_error_errno(hid_device, r, "Failed to get syspath for HID device: %m"); + + desc_path = path_join(sys_path, "report_descriptor"); + if (!desc_path) + return log_oom(); + + fd = open(desc_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NOCTTY); + if (fd < 0) + return log_device_error_errno(hid_device, errno, + "Failed to open report descriptor at '%s': %m", desc_path); + + desc_len = read(fd, desc, sizeof(desc)); + if (desc_len < 0) + return log_device_error_errno(hid_device, errno, + "Failed to read report descriptor at '%s': %m", desc_path); + if (desc_len == 0) + return log_device_debug_errno(hid_device, SYNTHETIC_ERRNO(EINVAL), + "Empty report descriptor at '%s'.", desc_path); + + r = is_fido_security_token_desc(desc, desc_len); + if (r < 0) + return log_device_debug_errno(hid_device, r, + "Failed to parse report descriptor at '%s'.", desc_path); + if (r > 0) { + printf("ID_FIDO_TOKEN=1\n"); + printf("ID_SECURITY_TOKEN=1\n"); + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/fido_id/fido_id_desc.c b/src/udev/fido_id/fido_id_desc.c new file mode 100644 index 0000000..2dfa759 --- /dev/null +++ b/src/udev/fido_id/fido_id_desc.c @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' */ + +#include <errno.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#include "fido_id_desc.h" + +#define HID_RPTDESC_FIRST_BYTE_LONG_ITEM 0xfeu +#define HID_RPTDESC_TYPE_GLOBAL 0x1u +#define HID_RPTDESC_TYPE_LOCAL 0x2u +#define HID_RPTDESC_TAG_USAGE_PAGE 0x0u +#define HID_RPTDESC_TAG_USAGE 0x0u + +/* + * HID usage for FIDO CTAP1 ("U2F") and CTAP2 security tokens. + * https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-u2f_hid.h-v1.0-ps-20141009.txt + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#usb-discovery + * https://www.usb.org/sites/default/files/hutrr48.pdf + */ +#define FIDO_FULL_USAGE_CTAPHID 0xf1d00001u + +/* + * Parses a HID report descriptor and identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on their + * declared usage. + * A positive return value indicates that the report descriptor belongs to a FIDO security token. + * https://www.usb.org/sites/default/files/documents/hid1_11.pdf (Section 6.2.2) + */ +int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len) { + uint32_t usage = 0; + + for (size_t pos = 0; pos < desc_len; ) { + uint8_t tag, type, size_code; + size_t size; + uint32_t value; + + /* Report descriptors consists of short items (1-5 bytes) and long items (3-258 bytes). */ + if (desc[pos] == HID_RPTDESC_FIRST_BYTE_LONG_ITEM) { + /* No long items are defined in the spec; skip them. + * The length of the data in a long item is contained in the byte after the long + * item tag. The header consists of three bytes: special long item tag, length, + * actual tag. */ + if (pos + 1 >= desc_len) + return -EINVAL; + pos += desc[pos + 1] + 3; + continue; + } + + /* The first byte of a short item encodes tag, type and size. */ + tag = desc[pos] >> 4; /* Bits 7 to 4 */ + type = (desc[pos] >> 2) & 0x3; /* Bits 3 and 2 */ + size_code = desc[pos] & 0x3; /* Bits 1 and 0 */ + /* Size is coded as follows: + * 0 -> 0 bytes, 1 -> 1 byte, 2 -> 2 bytes, 3 -> 4 bytes + */ + size = size_code < 3 ? size_code : 4; + /* Consume header byte. */ + pos++; + + /* Extract the item value coded on size bytes. */ + if (pos + size > desc_len) + return -EINVAL; + value = 0; + for (size_t i = 0; i < size; i++) + value |= (uint32_t) desc[pos + i] << (8 * i); + /* Consume value bytes. */ + pos += size; + + if (type == HID_RPTDESC_TYPE_GLOBAL && tag == HID_RPTDESC_TAG_USAGE_PAGE) { + /* A usage page is a 16 bit value coded on at most 16 bits. */ + if (size > 2) + return -EINVAL; + /* A usage page sets the upper 16 bits of a following usage. */ + usage = (value & 0x0000ffffu) << 16; + } + + if (type == HID_RPTDESC_TYPE_LOCAL && tag == HID_RPTDESC_TAG_USAGE) { + /* A usage is a 32 bit value, but is prepended with the current usage page if + * coded on less than 4 bytes (that is, at most 2 bytes). */ + if (size == 4) + usage = value; + else + usage = (usage & 0xffff0000u) | (value & 0x0000ffffu); + if (usage == FIDO_FULL_USAGE_CTAPHID) + return 1; + } + } + + return 0; +} diff --git a/src/udev/fido_id/fido_id_desc.h b/src/udev/fido_id/fido_id_desc.h new file mode 100644 index 0000000..57af57e --- /dev/null +++ b/src/udev/fido_id/fido_id_desc.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include <stddef.h> +#include <stdint.h> + +int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len); diff --git a/src/udev/fido_id/fuzz-fido-id-desc.c b/src/udev/fido_id/fuzz-fido-id-desc.c new file mode 100644 index 0000000..040d77a --- /dev/null +++ b/src/udev/fido_id/fuzz-fido-id-desc.c @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/hid.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +#include "fido_id_desc.h" +#include "fuzz.h" +#include "log.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + fuzz_setup_logging(); + + if (outside_size_range(size, 0, HID_MAX_DESCRIPTOR_SIZE)) + return 0; + + (void) is_fido_security_token_desc(data, size); + + return 0; +} diff --git a/src/udev/fido_id/test-fido-id-desc.c b/src/udev/fido_id/test-fido-id-desc.c new file mode 100644 index 0000000..36c777a --- /dev/null +++ b/src/udev/fido_id/test-fido-id-desc.c @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdint.h> +#include <stdlib.h> + +#include "fido_id_desc.h" +#include "macro.h" +#include "tests.h" + +TEST(is_fido_security_token_desc__fido) { + static const uint8_t FIDO_HID_DESC_1[] = { + 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(FIDO_HID_DESC_1, sizeof(FIDO_HID_DESC_1)) > 0); + + static const uint8_t FIDO_HID_DESC_2[] = { + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, + 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, + 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, + 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, + 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0, + 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(FIDO_HID_DESC_2, sizeof(FIDO_HID_DESC_2)) > 0); +} + +TEST(is_fido_security_token_desc__non_fido) { + /* Wrong usage page */ + static const uint8_t NON_FIDO_HID_DESC_1[] = { + 0x06, 0xd0, 0xf0, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_1, sizeof(NON_FIDO_HID_DESC_1)) == 0); + + /* Wrong usage */ + static const uint8_t NON_FIDO_HID_DESC_2[] = { + 0x06, 0xd0, 0xf1, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_2, sizeof(NON_FIDO_HID_DESC_2)) == 0); + + static const uint8_t NON_FIDO_HID_DESC_3[] = { + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, + 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, + 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, + 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, + 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_3, sizeof(NON_FIDO_HID_DESC_3)) == 0); +} + +TEST(is_fido_security_token_desc__invalid) { + /* Size coded on 1 byte, but no byte given */ + static const uint8_t INVALID_HID_DESC_1[] = { 0x01 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_1, sizeof(INVALID_HID_DESC_1)) < 0); + + /* Size coded on 2 bytes, but only 1 byte given */ + static const uint8_t INVALID_HID_DESC_2[] = { 0x02, 0x01 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_2, sizeof(INVALID_HID_DESC_2)) < 0); + + /* Size coded on 4 bytes, but only 3 bytes given */ + static const uint8_t INVALID_HID_DESC_3[] = { 0x03, 0x01, 0x02, 0x03 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_3, sizeof(INVALID_HID_DESC_3)) < 0); + + /* Long item without a size byte */ + static const uint8_t INVALID_HID_DESC_4[] = { 0xfe }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_4, sizeof(INVALID_HID_DESC_4)) < 0); + + /* Usage pages are coded on at most 2 bytes */ + static const uint8_t INVALID_HID_DESC_5[] = { 0x07, 0x01, 0x02, 0x03, 0x04 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_5, sizeof(INVALID_HID_DESC_5)) < 0); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/udev/fuzz-udev-rule-parse-value.c b/src/udev/fuzz-udev-rule-parse-value.c new file mode 100644 index 0000000..1817c15 --- /dev/null +++ b/src/udev/fuzz-udev-rule-parse-value.c @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <string.h> + +#include "alloc-util.h" +#include "fuzz.h" +#include "udev-rules.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_free_ char *str = NULL; + int r; + char *value = UINT_TO_PTR(0x12345678U); + char *endpos = UINT_TO_PTR(0x87654321U); + + fuzz_setup_logging(); + + assert_se(str = malloc(size + 1)); + memcpy(str, data, size); + str[size] = '\0'; + + r = udev_rule_parse_value(str, &value, &endpos); + if (r < 0) { + /* not modified on failure */ + assert_se(value == UINT_TO_PTR(0x12345678U)); + assert_se(endpos == UINT_TO_PTR(0x87654321U)); + } else { + assert_se(endpos <= str + size); + assert_se(endpos > str + 1); + } + + return 0; +} diff --git a/src/udev/fuzz-udev-rules.c b/src/udev/fuzz-udev-rules.c new file mode 100644 index 0000000..0a1056d --- /dev/null +++ b/src/udev/fuzz-udev-rules.c @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdio.h> + +#include "fd-util.h" +#include "fs-util.h" +#include "fuzz.h" +#include "tests.h" +#include "tmpfile-util.h" +#include "udev-rules.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-udev-rules.XXXXXX"; + int r; + + if (outside_size_range(size, 0, 65536)) + return 0; + + fuzz_setup_logging(); + + assert_se(fmkostemp_safe(filename, "r+", &f) == 0); + if (size != 0) + assert_se(fwrite(data, size, 1, f) == 1); + fflush(f); + + assert_se(rules = udev_rules_new(RESOLVE_NAME_EARLY)); + r = udev_rules_parse_file(rules, filename, /* extra_checks = */ false, NULL); + log_info_errno(r, "Parsing %s: %m", filename); + assert_se(r >= 0 || /* OK */ + r == -ENOBUFS); /* line length exceeded */ + + return 0; +} diff --git a/src/udev/fuzz-udev-rules.options b/src/udev/fuzz-udev-rules.options new file mode 100644 index 0000000..678d526 --- /dev/null +++ b/src/udev/fuzz-udev-rules.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65536 diff --git a/src/udev/generate-keyboard-keys-gperf.sh b/src/udev/generate-keyboard-keys-gperf.sh new file mode 100755 index 0000000..9f4364c --- /dev/null +++ b/src/udev/generate-keyboard-keys-gperf.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eu + +# shellcheck disable=SC1004 +awk ' + BEGIN { + print "%{\n\ +#if __GNUC__ >= 7\n\ +_Pragma(\"GCC diagnostic ignored \\\"-Wimplicit-fallthrough\\\"\")\n\ +#endif\n\ +%}" + print "struct key_name { const char* name; unsigned short id; };" + print "%null-strings" + print "%%" + } + + /^KEY_/ { print tolower(substr($1 ,5)) ", " $1 } + { print tolower($1) ", " $1 } +' <"${1:?}" diff --git a/src/udev/generate-keyboard-keys-list.sh b/src/udev/generate-keyboard-keys-list.sh new file mode 100755 index 0000000..ead3113 --- /dev/null +++ b/src/udev/generate-keyboard-keys-list.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eu +set -o pipefail + +${1:?} -dM -include linux/input.h - </dev/null | awk ' + /\<(KEY_(MAX|MIN_INTERESTING))|(BTN_(MISC|MOUSE|JOYSTICK|GAMEPAD|DIGI|WHEEL|TRIGGER_HAPPY))\>/ { next } + /^#define[ \t]+(KEY|BTN)_[^ ]+[ \t]+[0-9BK]/ { print $2 } +' diff --git a/src/udev/iocost/iocost.c b/src/udev/iocost/iocost.c new file mode 100644 index 0000000..2b2633e --- /dev/null +++ b/src/udev/iocost/iocost.c @@ -0,0 +1,321 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stdio.h> +#include <unistd.h> + +#include "sd-device.h" + +#include "alloc-util.h" +#include "build.h" +#include "cgroup-util.h" +#include "conf-parser.h" +#include "device-util.h" +#include "devnum-util.h" +#include "main-func.h" +#include "path-util.h" +#include "pretty-print.h" +#include "udev-util.h" +#include "verbs.h" + +static char *arg_target_solution = NULL; +STATIC_DESTRUCTOR_REGISTER(arg_target_solution, freep); + +static int parse_config(void) { + static const ConfigTableItem items[] = { + { "IOCost", "TargetSolution", config_parse_string, 0, &arg_target_solution }, + }; + int r; + + r = config_parse( + NULL, + "/etc/udev/iocost.conf", + NULL, + "IOCost\0", + config_item_table_lookup, + items, + CONFIG_PARSE_WARN, + NULL, + NULL); + if (r < 0) + return r; + + if (!arg_target_solution) { + arg_target_solution = strdup("naive"); + if (!arg_target_solution) + return log_oom(); + } + + log_debug("Target solution: %s", arg_target_solution); + return 0; +} + +static int help(void) { + printf("%s [OPTIONS...]\n\n" + "Set up iocost model and qos solutions for block devices\n" + "\nCommands:\n" + " apply <path> [SOLUTION] Apply solution for the device if\n" + " found, do nothing otherwise\n" + " query <path> Query the known solution for\n" + " the device\n" + "\nOptions:\n" + " -h --help Show this help\n" + " --version Show package version\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + {} + }; + + int c; + + assert(argc >= 1); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +static int get_known_solutions(sd_device *device, int log_level, char ***ret_solutions, const char **ret_selected) { + _cleanup_free_ char **s = NULL; + const char *value, *found; + int r; + + assert(ret_solutions); + assert(ret_selected); + + r = sd_device_get_property_value(device, "IOCOST_SOLUTIONS", &value); + if (r == -ENOENT) + return log_device_full_errno(device, log_level, r, "No iocost solution found for device."); + if (r < 0) + return log_device_error_errno(device, r, "Failed to query solutions from device: %m"); + + s = strv_split(value, WHITESPACE); + if (!s) + return log_oom(); + if (strv_isempty(s)) + return log_device_error_errno(device, SYNTHETIC_ERRNO(EINVAL), + "IOCOST_SOLUTIONS exists in hwdb but is empty."); + + found = strv_find(s, arg_target_solution); + if (found) { + *ret_selected = found; + log_device_debug(device, "Selected solution based on target solution: %s", *ret_selected); + } else { + *ret_selected = s[0]; + log_device_debug(device, "Selected first available solution: %s", *ret_selected); + } + + *ret_solutions = TAKE_PTR(s); + return 0; +} + +static int query_named_solution( + sd_device *device, + const char *name, + const char **ret_model, + const char **ret_qos) { + + _cleanup_free_ char *upper_name = NULL, *qos_key = NULL, *model_key = NULL; + const char *qos, *model; + int r; + + assert(name); + assert(ret_qos); + assert(ret_model); + + upper_name = strdup(name); + if (!upper_name) + return log_oom(); + + ascii_strupper(upper_name); + string_replace_char(upper_name, '-', '_'); + + qos_key = strjoin("IOCOST_QOS_", upper_name); + if (!qos_key) + return log_oom(); + + model_key = strjoin("IOCOST_MODEL_", upper_name); + if (!model_key) + return log_oom(); + + r = sd_device_get_property_value(device, qos_key, &qos); + if (r == -ENOENT) + return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", qos_key); + if (r < 0) + return log_device_error_errno(device, r, "Failed to obtain QoS for iocost solution from device: %m"); + + r = sd_device_get_property_value(device, model_key, &model); + if (r == -ENOENT) + return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", model_key); + if (r < 0) + return log_device_error_errno(device, r, "Failed to obtain model for iocost solution from device: %m"); + + *ret_qos = qos; + *ret_model = model; + + return 0; +} + +static int apply_solution_for_path(const char *path, const char *name) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + _cleanup_strv_free_ char **solutions = NULL; + _cleanup_free_ char *qos = NULL, *model = NULL; + const char *qos_params, *model_params; + dev_t devnum; + int r; + + r = sd_device_new_from_path(&device, path); + if (r < 0) + return log_error_errno(r, "Error looking up device: %m"); + + r = sd_device_get_devnum(device, &devnum); + if (r < 0) + return log_device_error_errno(device, r, "Error getting devnum: %m"); + + if (!name) { + r = get_known_solutions(device, LOG_DEBUG, &solutions, &name); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + } + + r = query_named_solution(device, name, &model_params, &qos_params); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + if (asprintf(&qos, DEVNUM_FORMAT_STR " enable=1 ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), qos_params) < 0) + return log_oom(); + + if (asprintf(&model, DEVNUM_FORMAT_STR " model=linear ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), model_params) < 0) + return log_oom(); + + log_debug("Applying iocost parameters to %s using solution '%s'\n" + "\tio.cost.qos: %s\n" + "\tio.cost.model: %s\n", + path, name, qos, model); + + r = cg_set_attribute("io", NULL, "io.cost.qos", qos); + if (r < 0) { + log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.qos: %m"); + return r == -ENOENT ? 0 : r; + } + + r = cg_set_attribute("io", NULL, "io.cost.model", model); + if (r < 0) { + log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.model: %m"); + return r == -ENOENT ? 0 : r; + } + + return 0; +} + +static int query_solutions_for_path(const char *path) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + _cleanup_strv_free_ char **solutions = NULL; + const char *selected_solution, *model_name; + int r; + + r = sd_device_new_from_path(&device, path); + if (r < 0) + return log_error_errno(r, "Error looking up device: %m"); + + r = device_get_model_string(device, &model_name); + if (r == -ENOENT) { + log_device_info(device, "Device model not found"); + return 0; + } + if (r < 0) + return log_device_error_errno(device, r, "Model name for device %s is unknown", path); + + r = get_known_solutions(device, LOG_INFO, &solutions, &selected_solution); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + log_info("Known solutions for %s model name: \"%s\"\n" + "Preferred solution: %s\n" + "Solution that would be applied: %s", + path, model_name, + arg_target_solution, selected_solution); + + STRV_FOREACH(s, solutions) { + const char *model, *qos; + + if (query_named_solution(device, *s, &model, &qos) < 0) + continue; + + log_info("%s: io.cost.qos: %s\n" + "%s: io.cost.model: %s", *s, qos, *s, model); + } + + return 0; +} + +static int verb_query(int argc, char *argv[], void *userdata) { + return query_solutions_for_path(ASSERT_PTR(argv[1])); +} + +static int verb_apply(int argc, char *argv[], void *userdata) { + return apply_solution_for_path( + ASSERT_PTR(argv[1]), + argc > 2 ? ASSERT_PTR(argv[2]) : NULL); +} + +static int iocost_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "query", 2, 2, 0, verb_query }, + { "apply", 2, 3, 0, verb_apply }, + {}, + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = parse_config(); + if (r < 0) + return r; + + return iocost_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/iocost/iocost.conf b/src/udev/iocost/iocost.conf new file mode 100644 index 0000000..c0eeea3 --- /dev/null +++ b/src/udev/iocost/iocost.conf @@ -0,0 +1,20 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Entries in this file show the compile time defaults. Local configuration +# should be created by either modifying this file (or a copy of it placed in +# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in +# the /etc/udev/iocost.conf.d/ directory. The latter is generally recommended. +# Defaults can be restored by simply deleting the main configuration file and +# all drop-ins located in /etc/. +# +# Use 'systemd-analyze cat-config udev/iocost.conf' to display the full config. +# +# See iocost.conf(5) for details. + +[IOCost] +#TargetSolution=naive diff --git a/src/udev/meson.build b/src/udev/meson.build new file mode 100644 index 0000000..824ec47 --- /dev/null +++ b/src/udev/meson.build @@ -0,0 +1,273 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +udevadm_sources = files( + 'udevadm-control.c', + 'udevadm-hwdb.c', + 'udevadm-info.c', + 'udevadm-lock.c', + 'udevadm-monitor.c', + 'udevadm-settle.c', + 'udevadm-test-builtin.c', + 'udevadm-test.c', + 'udevadm-trigger.c', + 'udevadm-util.c', + 'udevadm-verify.c', + 'udevadm-wait.c', + 'udevadm.c', + 'udevd.c', +) + +libudevd_core_sources = files( + 'net/link-config.c', + 'udev-ctrl.c', + 'udev-event.c', + 'udev-format.c', + 'udev-manager.c', + 'udev-node.c', + 'udev-rules.c', + 'udev-spawn.c', + 'udev-watch.c', + 'udev-worker.c', + 'udev-builtin-btrfs.c', + 'udev-builtin-hwdb.c', + 'udev-builtin-input_id.c', + 'udev-builtin-keyboard.c', + 'udev-builtin-net_driver.c', + 'udev-builtin-net_id.c', + 'udev-builtin-net_setup_link.c', + 'udev-builtin-path_id.c', + 'udev-builtin-usb_id.c', + 'udev-builtin.c', +) + +if conf.get('HAVE_KMOD') == 1 + libudevd_core_sources += files('udev-builtin-kmod.c') +endif + +if conf.get('HAVE_BLKID') == 1 + libudevd_core_sources += files('udev-builtin-blkid.c') +endif + +if conf.get('HAVE_ACL') == 1 + libudevd_core_sources += files('udev-builtin-uaccess.c') +endif + +############################################################ + +generate_keyboard_keys_list = find_program('generate-keyboard-keys-list.sh') +keyboard_keys_list_txt = custom_target( + 'keyboard-keys-list.txt', + output : 'keyboard-keys-list.txt', + command : [generate_keyboard_keys_list, cpp], + capture : true) + +generate_keyboard_keys_gperf = find_program('generate-keyboard-keys-gperf.sh') +fname = 'keyboard-keys-from-name.gperf' +gperf_file = custom_target( + fname, + input : keyboard_keys_list_txt, + output : fname, + command : [generate_keyboard_keys_gperf, '@INPUT@'], + capture : true) + +fname = 'keyboard-keys-from-name.h' +keyboard_keys_from_name_h = custom_target( + fname, + input : gperf_file, + output : fname, + command : [gperf, + '-L', 'ANSI-C', '-t', + '-N', 'keyboard_lookup_key', + '-H', 'hash_key_name', + '-p', '-C', + '@INPUT@'], + capture : true) + +############################################################ + +udev_link_gperf_gperf = files('net/link-config-gperf.gperf') + +link_config_gperf_c = custom_target( + 'link-config-gperf.c', + input : udev_link_gperf_gperf, + output : 'link-config-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +############################################################ + +if get_option('link-udev-shared') + udev_link_with = [libshared] + udev_rpath = pkglibdir +else + udev_link_with = [libshared_static, + libsystemd_static] + udev_rpath = '' +endif + +############################################################ + +libudevd_core = static_library( + 'udev-core', + libudevd_core_sources, + link_config_gperf_c, + keyboard_keys_from_name_h, + include_directories : includes + include_directories('net'), + link_with : udev_link_with, + dependencies : [libblkid, + libkmod, + userspace], + build_by_default : false) + +udev_dependencies = [ + libacl, + libblkid, + libkmod, + threads, +] + +udev_plugin_template = executable_template + { + 'public' : true, + 'link_with' : udev_link_with, + 'install_rpath' : udev_rpath, + 'install_dir' : udevlibexecdir, +} + +udev_common_template = { + 'link_with' : [ + libshared, + libudevd_core, + ], + 'dependencies' : [ + libacl, + threads, + ], +} +udev_test_template = test_template + udev_common_template +udev_fuzz_template = fuzz_template + udev_common_template + +executables += [ + executable_template + { + 'name' : 'udevadm', + 'public' : true, + 'sources' : udevadm_sources, + 'link_with' : [libudevd_core], + 'dependencies' : udev_dependencies, + 'install_rpath' : udev_rpath, + }, + udev_plugin_template + { + 'name' : 'ata_id', + 'sources' : files('ata_id/ata_id.c'), + }, + udev_plugin_template + { + 'name' : 'cdrom_id', + 'sources' : files('cdrom_id/cdrom_id.c'), + }, + udev_plugin_template + { + 'name' : 'dmi_memory_id', + 'conditions' : ['HAVE_DMI'], + 'sources' : files('dmi_memory_id/dmi_memory_id.c'), + }, + udev_plugin_template + { + 'name' : 'fido_id', + 'sources' : files( + 'fido_id/fido_id.c', + 'fido_id/fido_id_desc.c', + ), + }, + udev_plugin_template + { + 'name' : 'iocost', + 'sources' : files('iocost/iocost.c'), + }, + udev_plugin_template + { + 'name' : 'mtd_probe', + 'sources' : files( + 'mtd_probe/mtd_probe.c', + 'mtd_probe/probe_smartmedia.c', + ), + }, + udev_plugin_template + { + 'name' : 'scsi_id', + 'sources' : files( + 'scsi_id/scsi_id.c', + 'scsi_id/scsi_serial.c', + ), + }, + udev_plugin_template + { + 'name' : 'v4l_id', + 'sources' : files('v4l_id/v4l_id.c'), + }, + test_template + { + 'sources' : files( + 'fido_id/test-fido-id-desc.c', + 'fido_id/fido_id_desc.c', + ), + 'suite' : 'udev', + }, + udev_test_template + { + 'sources' : files('net/test-link-config-tables.c'), + 'suite' : 'udev', + }, + udev_test_template + { + 'sources' : files('test-udev-builtin.c'), + }, + udev_test_template + { + 'sources' : files('test-udev-format.c'), + }, + udev_test_template + { + 'sources' : files('test-udev-manager.c'), + }, + udev_test_template + { + 'sources' : files('test-udev-node.c'), + }, + udev_test_template + { + 'sources' : files('test-udev-rule-runner.c'), + 'dependencies' : udev_dependencies + [ + libselinux, + ], + 'type' : 'manual', + }, + udev_test_template + { + 'sources' : files('test-udev-rules.c'), + }, + udev_test_template + { + 'sources' : files('test-udev-spawn.c'), + }, + fuzz_template + { + 'sources' : files( + 'fido_id/fuzz-fido-id-desc.c', + 'fido_id/fido_id_desc.c', + ), + }, + udev_fuzz_template + { + 'sources' : files('net/fuzz-link-parser.c'), + }, + udev_fuzz_template + { + 'sources' : files('fuzz-udev-rule-parse-value.c'), + }, + udev_fuzz_template + { + 'sources' : files('fuzz-udev-rules.c'), + }, +] + +meson.add_install_script(sh, '-c', ln_s.format(bindir / 'udevadm', + libexecdir / 'systemd-udevd')) + +if install_sysconfdir_samples + install_data('udev.conf', + install_dir : configfiledir / 'udev') + install_data('iocost/iocost.conf', + install_dir : configfiledir / 'udev') +endif + +udev_pc = custom_target( + 'udev.pc', + input : 'udev.pc.in', + output : 'udev.pc', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : pkgconfigdatadir != 'no', + install_tag : 'devel', + install_dir : pkgconfigdatadir) + +if install_sysconfdir + install_emptydir(sysconfdir / 'udev/rules.d') +endif diff --git a/src/udev/mtd_probe/mtd_probe.c b/src/udev/mtd_probe/mtd_probe.c new file mode 100644 index 0000000..1035320 --- /dev/null +++ b/src/udev/mtd_probe/mtd_probe.c @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2010 - Maxim Levitsky + * + * mtd_probe is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mtd_probe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mtd_probe; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <mtd/mtd-user.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "build.h" +#include "fd-util.h" +#include "main-func.h" +#include "mtd_probe.h" + +static const char *arg_device = NULL; + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + {} + }; + int c; + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + case 'h': + printf("%s /dev/mtd[n]\n\n" + " -h --help Show this help text\n" + " --version Show package version\n", + program_invocation_short_name); + return 0; + case 'v': + return version(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (argc > 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error: unexpected argument."); + + arg_device = argv[optind]; + return 1; +} + +static int run(int argc, char** argv) { + _cleanup_close_ int mtd_fd = -EBADF; + mtd_info_t mtd_info; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + mtd_fd = open(argv[1], O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (mtd_fd < 0) + return log_error_errno(errno, "Failed to open: %m"); + + if (ioctl(mtd_fd, MEMGETINFO, &mtd_info) < 0) + return log_error_errno(errno, "MEMGETINFO ioctl failed: %m"); + + return probe_smart_media(mtd_fd, &mtd_info); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/mtd_probe/mtd_probe.h b/src/udev/mtd_probe/mtd_probe.h new file mode 100644 index 0000000..ae03a7d --- /dev/null +++ b/src/udev/mtd_probe/mtd_probe.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * Copyright © 2010 - Maxim Levitsky + * + * mtd_probe is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mtd_probe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mtd_probe; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include <mtd/mtd-user.h> + +#include "macro.h" + +/* Full oob structure as written on the flash */ +struct sm_oob { + uint32_t reserved; + uint8_t data_status; + uint8_t block_status; + uint8_t lba_copy1[2]; + uint8_t ecc2[3]; + uint8_t lba_copy2[2]; + uint8_t ecc1[3]; +} _packed_; + +/* one sector is always 512 bytes, but it can consist of two nand pages */ +#define SM_SECTOR_SIZE 512 + +/* oob area is also 16 bytes, but might be from two pages */ +#define SM_OOB_SIZE 16 + +/* This is maximum zone size, and all devices that have more that one zone + have this size */ +#define SM_MAX_ZONE_SIZE 1024 + +/* support for small page nand */ +#define SM_SMALL_PAGE 256 +#define SM_SMALL_OOB_SIZE 8 + +int probe_smart_media(int mtd_fd, mtd_info_t *info); diff --git a/src/udev/mtd_probe/probe_smartmedia.c b/src/udev/mtd_probe/probe_smartmedia.c new file mode 100644 index 0000000..368fab8 --- /dev/null +++ b/src/udev/mtd_probe/probe_smartmedia.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2010 - Maxim Levitsky + * + * mtd_probe is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mtd_probe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mtd_probe; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include <errno.h> +#include <fcntl.h> +#include <mtd/mtd-user.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "mtd_probe.h" + +static const uint8_t cis_signature[] = { + 0x01, 0x03, 0xD9, 0x01, 0xFF, 0x18, 0x02, 0xDF, 0x01, 0x20 +}; + +int probe_smart_media(int mtd_fd, mtd_info_t* info) { + int sector_size; + int block_size; + int size_in_megs; + int spare_count; + _cleanup_free_ uint8_t *cis_buffer = NULL; + int offset; + int cis_found = 0; + + cis_buffer = malloc(SM_SECTOR_SIZE); + if (!cis_buffer) + return log_oom(); + + if (info->type != MTD_NANDFLASH) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Not marked MTD_NANDFLASH."); + + sector_size = info->writesize; + block_size = info->erasesize; + size_in_megs = info->size / (1024 * 1024); + + if (!IN_SET(sector_size, SM_SECTOR_SIZE, SM_SMALL_PAGE)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Unexpected sector size: %i", sector_size); + + switch (size_in_megs) { + case 1: + case 2: + spare_count = 6; + break; + case 4: + spare_count = 12; + break; + default: + spare_count = 24; + break; + } + + for (offset = 0; offset < block_size * spare_count; offset += sector_size) { + (void) lseek(mtd_fd, SEEK_SET, offset); + + if (read(mtd_fd, cis_buffer, SM_SECTOR_SIZE) == SM_SECTOR_SIZE) { + cis_found = 1; + break; + } + } + + if (!cis_found) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "CIS not found"); + + if (memcmp(cis_buffer, cis_signature, sizeof(cis_signature)) != 0 && + memcmp(cis_buffer + SM_SMALL_PAGE, cis_signature, sizeof(cis_signature)) != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "CIS signature didn't match"); + + printf("MTD_FTL=smartmedia\n"); + return 0; +} diff --git a/src/udev/net/fuzz-link-parser.c b/src/udev/net/fuzz-link-parser.c new file mode 100644 index 0000000..2833162 --- /dev/null +++ b/src/udev/net/fuzz-link-parser.c @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fs-util.h" +#include "fuzz.h" +#include "link-config.h" +#include "tmpfile-util.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(link_config_ctx_freep) LinkConfigContext *ctx = NULL; + _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-link-config.XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + + if (outside_size_range(size, 0, 65536)) + return 0; + + fuzz_setup_logging(); + + assert_se(fmkostemp_safe(filename, "r+", &f) == 0); + if (size != 0) + assert_se(fwrite(data, size, 1, f) == 1); + + fflush(f); + assert_se(link_config_ctx_new(&ctx) >= 0); + (void) link_load_one(ctx, filename); + return 0; +} diff --git a/src/udev/net/fuzz-link-parser.options b/src/udev/net/fuzz-link-parser.options new file mode 100644 index 0000000..678d526 --- /dev/null +++ b/src/udev/net/fuzz-link-parser.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65536 diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf new file mode 100644 index 0000000..240f16e --- /dev/null +++ b/src/udev/net/link-config-gperf.gperf @@ -0,0 +1,118 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"") +#endif +#include <stddef.h> +#include "conf-parser.h" +#include "ethtool-util.h" +#include "link-config.h" +#include "net-condition.h" +#include "netif-sriov.h" +#include "socket-util.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name link_config_gperf_hash +%define lookup-function-name link_config_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Match.MACAddress, config_parse_hw_addrs, 0, offsetof(LinkConfig, match.hw_addr) +Match.PermanentMACAddress, config_parse_hw_addrs, 0, offsetof(LinkConfig, match.permanent_hw_addr) +Match.OriginalName, config_parse_match_ifnames, 0, offsetof(LinkConfig, match.ifname) +Match.Path, config_parse_match_strv, 0, offsetof(LinkConfig, match.path) +Match.Driver, config_parse_match_strv, 0, offsetof(LinkConfig, match.driver) +Match.Type, config_parse_match_strv, 0, offsetof(LinkConfig, match.iftype) +Match.Kind, config_parse_match_strv, 0, offsetof(LinkConfig, match.kind) +Match.Property, config_parse_match_property, 0, offsetof(LinkConfig, match.property) +Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(LinkConfig, conditions) +Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(LinkConfig, conditions) +Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(LinkConfig, conditions) +Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(LinkConfig, conditions) +Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(LinkConfig, conditions) +Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(LinkConfig, conditions) +Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(LinkConfig, conditions) +Link.Description, config_parse_string, 0, offsetof(LinkConfig, description) +Link.MACAddressPolicy, config_parse_mac_address_policy, 0, offsetof(LinkConfig, mac_address_policy) +Link.MACAddress, config_parse_hw_addr, 0, offsetof(LinkConfig, hw_addr) +Link.NamePolicy, config_parse_name_policy, 0, offsetof(LinkConfig, name_policy) +Link.Name, config_parse_ifname, 0, offsetof(LinkConfig, name) +Link.AlternativeName, config_parse_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(LinkConfig, alternative_names) +Link.AlternativeNamesPolicy, config_parse_alternative_names_policy, 0, offsetof(LinkConfig, alternative_names_policy) +Link.Alias, config_parse_ifalias, 0, offsetof(LinkConfig, alias) +Link.TransmitQueues, config_parse_rx_tx_queues, 0, offsetof(LinkConfig, txqueues) +Link.ReceiveQueues, config_parse_rx_tx_queues, 0, offsetof(LinkConfig, rxqueues) +Link.TransmitQueueLength, config_parse_txqueuelen, 0, offsetof(LinkConfig, txqueuelen) +Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(LinkConfig, mtu) +Link.BitsPerSecond, config_parse_si_uint64, 0, offsetof(LinkConfig, speed) +Link.Duplex, config_parse_duplex, 0, offsetof(LinkConfig, duplex) +Link.AutoNegotiation, config_parse_tristate, 0, offsetof(LinkConfig, autonegotiation) +Link.WakeOnLan, config_parse_wol, 0, offsetof(LinkConfig, wol) +Link.WakeOnLanPassword, config_parse_wol_password, 0, 0 +Link.Port, config_parse_port, 0, offsetof(LinkConfig, port) +Link.ReceiveChecksumOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_RXCSUM]) +Link.TransmitChecksumOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_TXCSUM]) +Link.GenericSegmentationOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_GSO]) +Link.TCPSegmentationOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_TSO]) +Link.TCP6SegmentationOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_TSO6]) +Link.UDPSegmentationOffload, config_parse_warn_compat, DISABLED_LEGACY, 0 +Link.GenericReceiveOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_GRO]) +Link.GenericReceiveOffloadHardware, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_GRO_HW]) +Link.LargeReceiveOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_LRO]) +Link.ReceiveVLANCTAGHardwareAcceleration, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_CTAG_RX]) +Link.TransmitVLANCTAGHardwareAcceleration, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_CTAG_TX]) +Link.ReceiveVLANCTAGFilter, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_CTAG_FILTER]) +Link.TransmitVLANSTAGHardwareAcceleration, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_STAG_TX]) +Link.NTupleFilter, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_NTUPLE]) +Link.RxChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.rx) +Link.TxChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.tx) +Link.OtherChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.other) +Link.CombinedChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.combined) +Link.Advertise, config_parse_advertise, 0, offsetof(LinkConfig, advertise) +Link.RxBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.rx) +Link.RxMiniBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.rx_mini) +Link.RxJumboBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.rx_jumbo) +Link.TxBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.tx) +Link.RxFlowControl, config_parse_tristate, 0, offsetof(LinkConfig, rx_flow_control) +Link.TxFlowControl, config_parse_tristate, 0, offsetof(LinkConfig, tx_flow_control) +Link.AutoNegotiationFlowControl, config_parse_tristate, 0, offsetof(LinkConfig, autoneg_flow_control) +Link.GenericSegmentOffloadMaxBytes, config_parse_iec_size, 0, offsetof(LinkConfig, gso_max_size) +Link.GenericSegmentOffloadMaxSegments, config_parse_uint32, 0, offsetof(LinkConfig, gso_max_segments) +Link.RxCoalesceSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs) +Link.RxMaxCoalescedFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames) +Link.RxCoalesceIrqSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs_irq) +Link.RxMaxCoalescedIrqFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames_irq) +Link.TxCoalesceSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs) +Link.TxMaxCoalescedFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames) +Link.TxCoalesceIrqSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs_irq) +Link.TxMaxCoalescedIrqFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames_irq) +Link.StatisticsBlockCoalesceSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.stats_block_coalesce_usecs) +Link.UseAdaptiveRxCoalesce, config_parse_tristate, 0, offsetof(LinkConfig, coalesce.use_adaptive_rx_coalesce) +Link.UseAdaptiveTxCoalesce, config_parse_tristate, 0, offsetof(LinkConfig, coalesce.use_adaptive_tx_coalesce) +Link.CoalescePacketRateLow, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.pkt_rate_low) +Link.RxCoalesceLowSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs_low) +Link.RxMaxCoalescedLowFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames_low) +Link.TxCoalesceLowSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs_low) +Link.TxMaxCoalescedLowFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames_low) +Link.CoalescePacketRateHigh, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.pkt_rate_high) +Link.RxCoalesceHighSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs_high) +Link.RxMaxCoalescedHighFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames_high) +Link.TxCoalesceHighSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs_high) +Link.TxMaxCoalescedHighFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames_high) +Link.CoalescePacketRateSampleIntervalSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rate_sample_interval) +Link.MDI, config_parse_mdi, 0, offsetof(LinkConfig, mdi) +Link.SR-IOVVirtualFunctions, config_parse_sr_iov_num_vfs, 0, offsetof(LinkConfig, sr_iov_num_vfs) +SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.VLANId, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.QualityOfService, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.VLANProtocol, config_parse_sr_iov_vlan_proto, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.MACSpoofCheck, config_parse_sr_iov_boolean, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.QueryReceiveSideScaling, config_parse_sr_iov_boolean, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.Trust, config_parse_sr_iov_boolean, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.LinkState, config_parse_sr_iov_link_state, 0, offsetof(LinkConfig, sr_iov_by_section) +SR-IOV.MACAddress, config_parse_sr_iov_mac, 0, offsetof(LinkConfig, sr_iov_by_section) diff --git a/src/udev/net/link-config.c b/src/udev/net/link-config.c new file mode 100644 index 0000000..910ec27 --- /dev/null +++ b/src/udev/net/link-config.c @@ -0,0 +1,1133 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/netdevice.h> +#include <netinet/ether.h> +#include <unistd.h> + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "alloc-util.h" +#include "arphrd-util.h" +#include "conf-files.h" +#include "conf-parser.h" +#include "constants.h" +#include "creds-util.h" +#include "device-private.h" +#include "device-util.h" +#include "ethtool-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "link-config.h" +#include "log-link.h" +#include "memory-util.h" +#include "net-condition.h" +#include "netif-sriov.h" +#include "netif-util.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "path-lookup.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "random-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "utf8.h" + +struct LinkConfigContext { + LIST_HEAD(LinkConfig, configs); + int ethtool_fd; + Hashmap *stats_by_path; +}; + +static LinkConfig* link_config_free(LinkConfig *config) { + if (!config) + return NULL; + + free(config->filename); + strv_free(config->dropins); + + net_match_clear(&config->match); + condition_free_list(config->conditions); + + free(config->description); + free(config->name_policy); + free(config->name); + strv_free(config->alternative_names); + free(config->alternative_names_policy); + free(config->alias); + free(config->wol_password_file); + erase_and_free(config->wol_password); + + ordered_hashmap_free_with_destructor(config->sr_iov_by_section, sr_iov_free); + + return mfree(config); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(LinkConfig*, link_config_free); + +static void link_configs_free(LinkConfigContext *ctx) { + if (!ctx) + return; + + ctx->stats_by_path = hashmap_free(ctx->stats_by_path); + + LIST_FOREACH(configs, config, ctx->configs) + link_config_free(config); +} + +LinkConfigContext *link_config_ctx_free(LinkConfigContext *ctx) { + if (!ctx) + return NULL; + + safe_close(ctx->ethtool_fd); + link_configs_free(ctx); + return mfree(ctx); +} + +int link_config_ctx_new(LinkConfigContext **ret) { + _cleanup_(link_config_ctx_freep) LinkConfigContext *ctx = NULL; + + if (!ret) + return -EINVAL; + + ctx = new(LinkConfigContext, 1); + if (!ctx) + return -ENOMEM; + + *ctx = (LinkConfigContext) { + .ethtool_fd = -EBADF, + }; + + *ret = TAKE_PTR(ctx); + + return 0; +} + +static int link_parse_wol_password(LinkConfig *config, const char *str) { + _cleanup_(erase_and_freep) uint8_t *p = NULL; + int r; + + assert(config); + assert(str); + + assert_cc(sizeof(struct ether_addr) == SOPASS_MAX); + + p = new(uint8_t, SOPASS_MAX); + if (!p) + return -ENOMEM; + + /* Reuse parse_ether_addr(), as their formats are equivalent. */ + r = parse_ether_addr(str, (struct ether_addr*) p); + if (r < 0) + return r; + + erase_and_free(config->wol_password); + config->wol_password = TAKE_PTR(p); + return 0; +} + +static int link_read_wol_password_from_file(LinkConfig *config) { + _cleanup_(erase_and_freep) char *password = NULL; + int r; + + assert(config); + + if (!config->wol_password_file) + return 0; + + r = read_full_file_full( + AT_FDCWD, config->wol_password_file, UINT64_MAX, SIZE_MAX, + READ_FULL_FILE_SECURE | READ_FULL_FILE_WARN_WORLD_READABLE | READ_FULL_FILE_CONNECT_SOCKET, + NULL, &password, NULL); + if (r < 0) + return r; + + return link_parse_wol_password(config, password); +} + +static int link_read_wol_password_from_cred(LinkConfig *config) { + _cleanup_free_ char *base = NULL, *cred_name = NULL; + _cleanup_(erase_and_freep) char *password = NULL; + int r; + + assert(config); + assert(config->filename); + + if (config->wol == UINT32_MAX) + return 0; /* WakeOnLan= is not specified. */ + if (!FLAGS_SET(config->wol, WAKE_MAGICSECURE)) + return 0; /* secureon is not specified in WakeOnLan=. */ + if (config->wol_password) + return 0; /* WakeOnLanPassword= is specified. */ + if (config->wol_password_file) + return 0; /* a file name is specified in WakeOnLanPassword=, but failed to read it. */ + + r = path_extract_filename(config->filename, &base); + if (r < 0) + return r; + + cred_name = strjoin(base, ".wol.password"); + if (!cred_name) + return -ENOMEM; + + r = read_credential(cred_name, (void**) &password, NULL); + if (r == -ENOENT) + r = read_credential("wol.password", (void**) &password, NULL); + if (r < 0) + return r; + + return link_parse_wol_password(config, password); +} + +static int link_adjust_wol_options(LinkConfig *config) { + int r; + + assert(config); + + r = link_read_wol_password_from_file(config); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_warning_errno(r, "Failed to read WakeOnLan password from %s, ignoring: %m", config->wol_password_file); + + r = link_read_wol_password_from_cred(config); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_warning_errno(r, "Failed to read WakeOnLan password from credential, ignoring: %m"); + + if (config->wol != UINT32_MAX && config->wol_password) + /* Enable WAKE_MAGICSECURE flag when WakeOnLanPassword=. Note that when + * WakeOnLanPassword= is set without WakeOnLan=, then ethtool_set_wol() enables + * WAKE_MAGICSECURE flag and other flags are not changed. */ + config->wol |= WAKE_MAGICSECURE; + + return 0; +} + +int link_load_one(LinkConfigContext *ctx, const char *filename) { + _cleanup_(link_config_freep) LinkConfig *config = NULL; + _cleanup_hashmap_free_ Hashmap *stats_by_path = NULL; + _cleanup_free_ char *name = NULL; + const char *dropin_dirname; + size_t i; + int r; + + assert(ctx); + assert(filename); + + r = null_or_empty_path(filename); + if (r < 0) + return log_warning_errno(r, "Failed to check if \"%s\" is empty: %m", filename); + if (r > 0) { + log_debug("Skipping empty file: %s", filename); + return 0; + } + + name = strdup(filename); + if (!name) + return log_oom(); + + config = new(LinkConfig, 1); + if (!config) + return log_oom(); + + *config = (LinkConfig) { + .filename = TAKE_PTR(name), + .mac_address_policy = MAC_ADDRESS_POLICY_NONE, + .wol = UINT32_MAX, /* UINT32_MAX means do not change WOL setting. */ + .duplex = _DUP_INVALID, + .port = _NET_DEV_PORT_INVALID, + .autonegotiation = -1, + .rx_flow_control = -1, + .tx_flow_control = -1, + .autoneg_flow_control = -1, + .txqueuelen = UINT32_MAX, + .coalesce.use_adaptive_rx_coalesce = -1, + .coalesce.use_adaptive_tx_coalesce = -1, + .mdi = ETH_TP_MDI_INVALID, + .sr_iov_num_vfs = UINT32_MAX, + }; + + for (i = 0; i < ELEMENTSOF(config->features); i++) + config->features[i] = -1; + + dropin_dirname = strjoina(basename(filename), ".d"); + r = config_parse_many( + STRV_MAKE_CONST(filename), + NETWORK_DIRS, + dropin_dirname, + /* root = */ NULL, + "Match\0" + "Link\0" + "SR-IOV\0", + config_item_perf_lookup, link_config_gperf_lookup, + CONFIG_PARSE_WARN, config, &stats_by_path, + &config->dropins); + if (r < 0) + return r; /* config_parse_many() logs internally. */ + + if (ctx->stats_by_path) { + r = hashmap_move(ctx->stats_by_path, stats_by_path); + if (r < 0) + log_warning_errno(r, "Failed to save stats of '%s' and its drop-in configs, ignoring: %m", filename); + } else + ctx->stats_by_path = TAKE_PTR(stats_by_path); + + if (net_match_is_empty(&config->match) && !config->conditions) { + log_warning("%s: No valid settings found in the [Match] section, ignoring file. " + "To match all interfaces, add OriginalName=* in the [Match] section.", + filename); + return 0; + } + + if (!condition_test_list(config->conditions, environ, NULL, NULL, NULL)) { + log_debug("%s: Conditions do not match the system environment, skipping.", filename); + return 0; + } + + if (IN_SET(config->mac_address_policy, MAC_ADDRESS_POLICY_PERSISTENT, MAC_ADDRESS_POLICY_RANDOM) && + config->hw_addr.length > 0) + log_warning("%s: MACAddress= in [Link] section will be ignored when MACAddressPolicy= " + "is set to \"persistent\" or \"random\".", + filename); + + r = link_adjust_wol_options(config); + if (r < 0) + return r; /* link_adjust_wol_options() logs internally. */ + + r = sr_iov_drop_invalid_sections(config->sr_iov_num_vfs, config->sr_iov_by_section); + if (r < 0) + return r; /* sr_iov_drop_invalid_sections() logs internally. */ + + log_debug("Parsed configuration file \"%s\"", filename); + + LIST_PREPEND(configs, ctx->configs, TAKE_PTR(config)); + return 0; +} + +static int device_unsigned_attribute(sd_device *device, const char *attr, unsigned *type) { + const char *s; + int r; + + r = sd_device_get_sysattr_value(device, attr, &s); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to query %s: %m", attr); + + r = safe_atou(s, type); + if (r < 0) + return log_device_warning_errno(device, r, "Failed to parse %s \"%s\": %m", attr, s); + + log_device_debug(device, "Device has %s=%u", attr, *type); + return 0; +} + +int link_config_load(LinkConfigContext *ctx) { + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(ctx); + + link_configs_free(ctx); + + r = conf_files_list_strv(&files, ".link", NULL, 0, NETWORK_DIRS); + if (r < 0) + return log_error_errno(r, "failed to enumerate link files: %m"); + + STRV_FOREACH_BACKWARDS(f, files) + (void) link_load_one(ctx, *f); + + return 0; +} + +bool link_config_should_reload(LinkConfigContext *ctx) { + _cleanup_hashmap_free_ Hashmap *stats_by_path = NULL; + int r; + + assert(ctx); + + r = config_get_stats_by_path(".link", NULL, 0, NETWORK_DIRS, /* check_dropins = */ true, &stats_by_path); + if (r < 0) { + log_warning_errno(r, "Failed to get stats of .link files, ignoring: %m"); + return true; + } + + return !stats_by_path_equal(ctx->stats_by_path, stats_by_path); +} + +Link *link_free(Link *link) { + if (!link) + return NULL; + + sd_device_unref(link->device); + free(link->kind); + strv_free(link->altnames); + return mfree(link); +} + +int link_new(LinkConfigContext *ctx, sd_netlink **rtnl, sd_device *device, Link **ret) { + _cleanup_(link_freep) Link *link = NULL; + int r; + + assert(ctx); + assert(rtnl); + assert(device); + assert(ret); + + link = new(Link, 1); + if (!link) + return -ENOMEM; + + *link = (Link) { + .device = sd_device_ref(device), + }; + + r = sd_device_get_sysname(device, &link->ifname); + if (r < 0) + return r; + + r = sd_device_get_ifindex(device, &link->ifindex); + if (r < 0) + return r; + + r = sd_device_get_action(device, &link->action); + if (r < 0) + return r; + + r = device_unsigned_attribute(device, "name_assign_type", &link->name_assign_type); + if (r < 0) + log_link_debug_errno(link, r, "Failed to get \"name_assign_type\" attribute, ignoring: %m"); + + r = device_unsigned_attribute(device, "addr_assign_type", &link->addr_assign_type); + if (r < 0) + log_link_debug_errno(link, r, "Failed to get \"addr_assign_type\" attribute, ignoring: %m"); + + r = rtnl_get_link_info(rtnl, link->ifindex, &link->iftype, &link->flags, + &link->kind, &link->hw_addr, &link->permanent_hw_addr); + if (r < 0) + return r; + + if (link->hw_addr.length > 0 && link->permanent_hw_addr.length == 0) { + r = ethtool_get_permanent_hw_addr(&ctx->ethtool_fd, link->ifname, &link->permanent_hw_addr); + if (r < 0) + log_link_debug_errno(link, r, "Failed to get permanent hardware address, ignoring: %m"); + } + + r = sd_device_get_property_value(link->device, "ID_NET_DRIVER", &link->driver); + if (r < 0 && r != -ENOENT) + log_link_debug_errno(link, r, "Failed to get driver, ignoring: %m"); + + *ret = TAKE_PTR(link); + return 0; +} + +int link_get_config(LinkConfigContext *ctx, Link *link) { + int r; + + assert(ctx); + assert(link); + + /* Do not configure loopback interfaces by .link files. */ + if (link->flags & IFF_LOOPBACK) + return -ENOENT; + + LIST_FOREACH(configs, config, ctx->configs) { + r = net_match_config( + &config->match, + link->device, + &link->hw_addr, + &link->permanent_hw_addr, + link->driver, + link->iftype, + link->kind, + link->ifname, + /* alternative_names = */ NULL, + /* wlan_iftype = */ 0, + /* ssid = */ NULL, + /* bssid = */ NULL); + if (r < 0) + return r; + if (r == 0) + continue; + + if (config->match.ifname && !strv_contains(config->match.ifname, "*") && link->name_assign_type == NET_NAME_ENUM) + log_link_warning(link, "Config file %s is applied to device based on potentially unpredictable interface name.", + config->filename); + else + log_link_debug(link, "Config file %s is applied", config->filename); + + link->config = config; + return 0; + } + + return -ENOENT; +} + +static int link_apply_ethtool_settings(Link *link, int *ethtool_fd) { + LinkConfig *config; + const char *name; + int r; + + assert(link); + assert(link->config); + assert(ethtool_fd); + + config = link->config; + name = link->ifname; + + r = ethtool_set_glinksettings(ethtool_fd, name, + config->autonegotiation, config->advertise, + config->speed, config->duplex, config->port, config->mdi); + if (r < 0) { + if (config->autonegotiation >= 0) + log_link_warning_errno(link, r, "Could not %s auto negotiation, ignoring: %m", + enable_disable(config->autonegotiation)); + + if (!eqzero(config->advertise)) + log_link_warning_errno(link, r, "Could not set advertise mode, ignoring: %m"); + + if (config->speed > 0) + log_link_warning_errno(link, r, "Could not set speed to %"PRIu64"Mbps, ignoring: %m", + DIV_ROUND_UP(config->speed, 1000000)); + + if (config->duplex >= 0) + log_link_warning_errno(link, r, "Could not set duplex to %s, ignoring: %m", + duplex_to_string(config->duplex)); + + if (config->port >= 0) + log_link_warning_errno(link, r, "Could not set port to '%s', ignoring: %m", + port_to_string(config->port)); + + if (config->mdi != ETH_TP_MDI_INVALID) + log_link_warning_errno(link, r, "Could not set MDI-X to '%s', ignoring: %m", + mdi_to_string(config->mdi)); + } + + r = ethtool_set_wol(ethtool_fd, name, config->wol, config->wol_password); + if (r < 0) { + _cleanup_free_ char *str = NULL; + + (void) wol_options_to_string_alloc(config->wol, &str); + log_link_warning_errno(link, r, "Could not set WakeOnLan%s%s, ignoring: %m", + isempty(str) ? "" : " to ", strempty(str)); + } + + r = ethtool_set_features(ethtool_fd, name, config->features); + if (r < 0) + log_link_warning_errno(link, r, "Could not set offload features, ignoring: %m"); + + r = ethtool_set_channels(ethtool_fd, name, &config->channels); + if (r < 0) + log_link_warning_errno(link, r, "Could not set channels, ignoring: %m"); + + r = ethtool_set_nic_buffer_size(ethtool_fd, name, &config->ring); + if (r < 0) + log_link_warning_errno(link, r, "Could not set ring buffer, ignoring: %m"); + + r = ethtool_set_flow_control(ethtool_fd, name, config->rx_flow_control, config->tx_flow_control, config->autoneg_flow_control); + if (r < 0) + log_link_warning_errno(link, r, "Could not set flow control, ignoring: %m"); + + r = ethtool_set_nic_coalesce_settings(ethtool_fd, name, &config->coalesce); + if (r < 0) + log_link_warning_errno(link, r, "Could not set coalesce settings, ignoring: %m"); + + return 0; +} + +static bool hw_addr_is_valid(Link *link, const struct hw_addr_data *hw_addr) { + assert(link); + assert(hw_addr); + + switch (link->iftype) { + case ARPHRD_ETHER: + /* Refuse all zero and all 0xFF. */ + assert(hw_addr->length == ETH_ALEN); + return !ether_addr_is_null(&hw_addr->ether) && !ether_addr_is_broadcast(&hw_addr->ether); + + case ARPHRD_INFINIBAND: + /* The last 8 bytes cannot be zero. */ + assert(hw_addr->length == INFINIBAND_ALEN); + return !memeqzero(hw_addr->bytes + INFINIBAND_ALEN - 8, 8); + + default: + assert_not_reached(); + } +} + +static int link_generate_new_hw_addr(Link *link, struct hw_addr_data *ret) { + struct hw_addr_data hw_addr = HW_ADDR_NULL; + bool is_static = false; + uint8_t *p; + size_t len; + int r; + + assert(link); + assert(link->config); + assert(link->device); + assert(ret); + + if (link->hw_addr.length == 0) + goto finalize; + + if (link->config->mac_address_policy == MAC_ADDRESS_POLICY_NONE) { + log_link_debug(link, "Using static MAC address."); + hw_addr = link->config->hw_addr; + is_static = true; + goto finalize; + } + + if (!IN_SET(link->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND)) + goto finalize; + + switch (link->addr_assign_type) { + case NET_ADDR_SET: + log_link_debug(link, "MAC address on the device already set by userspace."); + goto finalize; + case NET_ADDR_STOLEN: + log_link_debug(link, "MAC address on the device already set based on another device."); + goto finalize; + case NET_ADDR_RANDOM: + case NET_ADDR_PERM: + break; + default: + log_link_warning(link, "Unknown addr_assign_type %u, ignoring", link->addr_assign_type); + goto finalize; + } + + if ((link->config->mac_address_policy == MAC_ADDRESS_POLICY_RANDOM) == (link->addr_assign_type == NET_ADDR_RANDOM)) { + log_link_debug(link, "MAC address on the device already matches policy \"%s\".", + mac_address_policy_to_string(link->config->mac_address_policy)); + goto finalize; + } + + hw_addr = (struct hw_addr_data) { + .length = arphrd_to_hw_addr_len(link->iftype), + }; + + switch (link->iftype) { + case ARPHRD_ETHER: + p = hw_addr.bytes; + len = hw_addr.length; + break; + case ARPHRD_INFINIBAND: + p = hw_addr.bytes + INFINIBAND_ALEN - 8; + len = 8; + break; + default: + assert_not_reached(); + } + + if (link->config->mac_address_policy == MAC_ADDRESS_POLICY_RANDOM) + /* We require genuine randomness here, since we want to make sure we won't collide with other + * systems booting up at the very same time. */ + for (;;) { + random_bytes(p, len); + if (hw_addr_is_valid(link, &hw_addr)) + break; + } + + else { + uint64_t result; + + r = net_get_unique_predictable_data(link->device, + naming_scheme_has(NAMING_STABLE_VIRTUAL_MACS), + &result); + if (r < 0) + return log_link_warning_errno(link, r, "Could not generate persistent MAC address: %m"); + + assert(len <= sizeof(result)); + memcpy(p, &result, len); + if (!hw_addr_is_valid(link, &hw_addr)) + return log_link_warning_errno(link, SYNTHETIC_ERRNO(EINVAL), + "Could not generate valid persistent MAC address: %m"); + } + +finalize: + + r = net_verify_hardware_address(link->ifname, is_static, link->iftype, &link->hw_addr, &hw_addr); + if (r < 0) + return r; + + if (hw_addr_equal(&link->hw_addr, &hw_addr)) { + *ret = HW_ADDR_NULL; + return 0; + } + + if (hw_addr.length > 0) + log_link_debug(link, "Applying %s MAC address: %s", + link->config->mac_address_policy == MAC_ADDRESS_POLICY_NONE ? "static" : + mac_address_policy_to_string(link->config->mac_address_policy), + HW_ADDR_TO_STR(&hw_addr)); + + *ret = hw_addr; + return 0; +} + +static int link_apply_rtnl_settings(Link *link, sd_netlink **rtnl) { + struct hw_addr_data hw_addr = {}; + LinkConfig *config; + int r; + + assert(link); + assert(link->config); + assert(rtnl); + + config = link->config; + + (void) link_generate_new_hw_addr(link, &hw_addr); + + r = rtnl_set_link_properties(rtnl, link->ifindex, config->alias, &hw_addr, + config->txqueues, config->rxqueues, config->txqueuelen, + config->mtu, config->gso_max_size, config->gso_max_segments); + if (r < 0) + log_link_warning_errno(link, r, + "Could not set Alias=, MACAddress=/MACAddressPolicy=, " + "TransmitQueues=, ReceiveQueues=, TransmitQueueLength=, MTUBytes=, " + "GenericSegmentOffloadMaxBytes= or GenericSegmentOffloadMaxSegments=, " + "ignoring: %m"); + + return 0; +} + +static bool enable_name_policy(void) { + static int cached = -1; + bool b; + int r; + + if (cached >= 0) + return cached; + + r = proc_cmdline_get_bool("net.ifnames", /* flags = */ 0, &b); + if (r < 0) + log_warning_errno(r, "Failed to parse net.ifnames= kernel command line option, ignoring: %m"); + if (r <= 0) + return (cached = true); + + if (!b) + log_info("Network interface NamePolicy= disabled on kernel command line."); + + return (cached = b); +} + +static int link_generate_new_name(Link *link) { + LinkConfig *config; + sd_device *device; + + assert(link); + assert(link->config); + assert(link->device); + + config = link->config; + device = link->device; + + if (link->action != SD_DEVICE_ADD) { + log_link_debug(link, "Skipping to apply Name= and NamePolicy= on '%s' uevent.", + device_action_to_string(link->action)); + goto no_rename; + } + + if (IN_SET(link->name_assign_type, NET_NAME_USER, NET_NAME_RENAMED) && + !naming_scheme_has(NAMING_ALLOW_RERENAMES)) { + log_link_debug(link, "Device already has a name given by userspace, not renaming."); + goto no_rename; + } + + if (enable_name_policy() && config->name_policy) + for (NamePolicy *policy = config->name_policy; *policy != _NAMEPOLICY_INVALID; policy++) { + const char *new_name = NULL; + + switch (*policy) { + case NAMEPOLICY_KERNEL: + if (link->name_assign_type != NET_NAME_PREDICTABLE) + continue; + + /* The kernel claims to have given a predictable name, keep it. */ + log_link_debug(link, "Policy *%s*: keeping predictable kernel name", + name_policy_to_string(*policy)); + goto no_rename; + case NAMEPOLICY_KEEP: + if (!IN_SET(link->name_assign_type, NET_NAME_USER, NET_NAME_RENAMED)) + continue; + + log_link_debug(link, "Policy *%s*: keeping existing userspace name", + name_policy_to_string(*policy)); + goto no_rename; + case NAMEPOLICY_DATABASE: + (void) sd_device_get_property_value(device, "ID_NET_NAME_FROM_DATABASE", &new_name); + break; + case NAMEPOLICY_ONBOARD: + (void) sd_device_get_property_value(device, "ID_NET_NAME_ONBOARD", &new_name); + break; + case NAMEPOLICY_SLOT: + (void) sd_device_get_property_value(device, "ID_NET_NAME_SLOT", &new_name); + break; + case NAMEPOLICY_PATH: + (void) sd_device_get_property_value(device, "ID_NET_NAME_PATH", &new_name); + break; + case NAMEPOLICY_MAC: + (void) sd_device_get_property_value(device, "ID_NET_NAME_MAC", &new_name); + break; + default: + assert_not_reached(); + } + if (ifname_valid(new_name)) { + log_link_debug(link, "Policy *%s* yields \"%s\".", name_policy_to_string(*policy), new_name); + link->new_name = new_name; + return 0; + } + } + + if (link->config->name) { + log_link_debug(link, "Policies didn't yield a name, using specified Name=%s.", link->config->name); + link->new_name = link->config->name; + return 0; + } + + log_link_debug(link, "Policies didn't yield a name and Name= is not given, not renaming."); +no_rename: + link->new_name = link->ifname; + return 0; +} + +static int link_generate_alternative_names(Link *link) { + _cleanup_strv_free_ char **altnames = NULL; + LinkConfig *config; + sd_device *device; + int r; + + assert(link); + config = ASSERT_PTR(link->config); + device = ASSERT_PTR(link->device); + assert(!link->altnames); + + if (link->action != SD_DEVICE_ADD) { + log_link_debug(link, "Skipping to apply AlternativeNames= and AlternativeNamesPolicy= on '%s' uevent.", + device_action_to_string(link->action)); + return 0; + } + + if (config->alternative_names) { + altnames = strv_copy(config->alternative_names); + if (!altnames) + return log_oom(); + } + + if (config->alternative_names_policy) + for (NamePolicy *p = config->alternative_names_policy; *p != _NAMEPOLICY_INVALID; p++) { + const char *n = NULL; + + switch (*p) { + case NAMEPOLICY_DATABASE: + (void) sd_device_get_property_value(device, "ID_NET_NAME_FROM_DATABASE", &n); + break; + case NAMEPOLICY_ONBOARD: + (void) sd_device_get_property_value(device, "ID_NET_NAME_ONBOARD", &n); + break; + case NAMEPOLICY_SLOT: + (void) sd_device_get_property_value(device, "ID_NET_NAME_SLOT", &n); + break; + case NAMEPOLICY_PATH: + (void) sd_device_get_property_value(device, "ID_NET_NAME_PATH", &n); + break; + case NAMEPOLICY_MAC: + (void) sd_device_get_property_value(device, "ID_NET_NAME_MAC", &n); + break; + default: + assert_not_reached(); + } + if (ifname_valid_full(n, IFNAME_VALID_ALTERNATIVE)) { + r = strv_extend(&altnames, n); + if (r < 0) + return log_oom(); + } + } + + link->altnames = TAKE_PTR(altnames); + return 0; +} + +static int sr_iov_configure(Link *link, sd_netlink **rtnl, SRIOV *sr_iov) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + int r; + + assert(link); + assert(rtnl); + assert(link->ifindex > 0); + + if (!*rtnl) { + r = sd_netlink_open(rtnl); + if (r < 0) + return r; + } + + r = sd_rtnl_message_new_link(*rtnl, &req, RTM_SETLINK, link->ifindex); + if (r < 0) + return r; + + r = sr_iov_set_netlink_message(sr_iov, req); + if (r < 0) + return r; + + r = sd_netlink_call(*rtnl, req, 0, NULL); + if (r < 0) + return r; + + return 0; +} + +static int link_apply_sr_iov_config(Link *link, sd_netlink **rtnl) { + SRIOV *sr_iov; + uint32_t n; + int r; + + assert(link); + assert(link->config); + assert(link->device); + + r = sr_iov_set_num_vfs(link->device, link->config->sr_iov_num_vfs, link->config->sr_iov_by_section); + if (r < 0) + log_link_warning_errno(link, r, "Failed to set the number of SR-IOV virtual functions, ignoring: %m"); + + if (ordered_hashmap_isempty(link->config->sr_iov_by_section)) + return 0; + + r = sr_iov_get_num_vfs(link->device, &n); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to get the number of SR-IOV virtual functions, ignoring [SR-IOV] sections: %m"); + return 0; + } + if (n == 0) { + log_link_warning(link, "No SR-IOV virtual function exists, ignoring [SR-IOV] sections: %m"); + return 0; + } + + ORDERED_HASHMAP_FOREACH(sr_iov, link->config->sr_iov_by_section) { + if (sr_iov->vf >= n) { + log_link_warning(link, "SR-IOV virtual function %"PRIu32" does not exist, ignoring.", sr_iov->vf); + continue; + } + + r = sr_iov_configure(link, rtnl, sr_iov); + if (r < 0) + log_link_warning_errno(link, r, + "Failed to configure SR-IOV virtual function %"PRIu32", ignoring: %m", + sr_iov->vf); + } + + return 0; +} + +int link_apply_config(LinkConfigContext *ctx, sd_netlink **rtnl, Link *link) { + int r; + + assert(ctx); + assert(rtnl); + assert(link); + + if (!IN_SET(link->action, SD_DEVICE_ADD, SD_DEVICE_BIND, SD_DEVICE_MOVE)) { + log_link_debug(link, "Skipping to apply .link settings on '%s' uevent.", + device_action_to_string(link->action)); + + link->new_name = link->ifname; + return 0; + } + + r = link_apply_ethtool_settings(link, &ctx->ethtool_fd); + if (r < 0) + return r; + + r = link_apply_rtnl_settings(link, rtnl); + if (r < 0) + return r; + + r = link_generate_new_name(link); + if (r < 0) + return r; + + r = link_generate_alternative_names(link); + if (r < 0) + return r; + + r = link_apply_sr_iov_config(link, rtnl); + if (r < 0) + return r; + + return 0; +} + +int config_parse_ifalias( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + char **s = ASSERT_PTR(data); + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *s = mfree(*s); + return 0; + } + + if (!ascii_is_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Interface alias is not ASCII clean, ignoring assignment: %s", rvalue); + return 0; + } + + if (strlen(rvalue) >= IFALIASZ) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Interface alias is too long, ignoring assignment: %s", rvalue); + return 0; + } + + return free_and_strdup_warn(s, rvalue); +} + +int config_parse_rx_tx_queues( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint32_t k, *v = data; + int r; + + if (isempty(rvalue)) { + *v = 0; + return 0; + } + + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s=, ignoring assignment: %s.", lvalue, rvalue); + return 0; + } + if (k == 0 || k > 4096) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid %s=, ignoring assignment: %s.", lvalue, rvalue); + return 0; + } + + *v = k; + return 0; +} + +int config_parse_txqueuelen( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + uint32_t k, *v = data; + int r; + + if (isempty(rvalue)) { + *v = UINT32_MAX; + return 0; + } + + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s=, ignoring assignment: %s.", lvalue, rvalue); + return 0; + } + if (k == UINT32_MAX) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid %s=, ignoring assignment: %s.", lvalue, rvalue); + return 0; + } + + *v = k; + return 0; +} + +int config_parse_wol_password( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + LinkConfig *config = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + config->wol_password = erase_and_free(config->wol_password); + config->wol_password_file = mfree(config->wol_password_file); + return 0; + } + + if (path_is_absolute(rvalue) && path_is_safe(rvalue)) { + config->wol_password = erase_and_free(config->wol_password); + return free_and_strdup_warn(&config->wol_password_file, rvalue); + } + + warn_file_is_world_accessible(filename, NULL, unit, line); + + r = link_parse_wol_password(config, rvalue); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s.", lvalue, rvalue); + return 0; + } + + config->wol_password_file = mfree(config->wol_password_file); + return 0; +} + +static const char* const mac_address_policy_table[_MAC_ADDRESS_POLICY_MAX] = { + [MAC_ADDRESS_POLICY_PERSISTENT] = "persistent", + [MAC_ADDRESS_POLICY_RANDOM] = "random", + [MAC_ADDRESS_POLICY_NONE] = "none", +}; + +DEFINE_STRING_TABLE_LOOKUP(mac_address_policy, MACAddressPolicy); +DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT( + config_parse_mac_address_policy, + mac_address_policy, + MACAddressPolicy, + MAC_ADDRESS_POLICY_NONE, + "Failed to parse MAC address policy"); + +DEFINE_CONFIG_PARSE_ENUMV(config_parse_name_policy, name_policy, NamePolicy, + _NAMEPOLICY_INVALID, + "Failed to parse interface name policy"); + +DEFINE_CONFIG_PARSE_ENUMV(config_parse_alternative_names_policy, alternative_names_policy, NamePolicy, + _NAMEPOLICY_INVALID, + "Failed to parse alternative names policy"); diff --git a/src/udev/net/link-config.h b/src/udev/net/link-config.h new file mode 100644 index 0000000..bab9d12 --- /dev/null +++ b/src/udev/net/link-config.h @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "condition.h" +#include "conf-parser.h" +#include "ethtool-util.h" +#include "hashmap.h" +#include "list.h" +#include "net-condition.h" +#include "netif-naming-scheme.h" + +typedef struct LinkConfigContext LinkConfigContext; +typedef struct LinkConfig LinkConfig; + +typedef enum MACAddressPolicy { + MAC_ADDRESS_POLICY_PERSISTENT, + MAC_ADDRESS_POLICY_RANDOM, + MAC_ADDRESS_POLICY_NONE, + _MAC_ADDRESS_POLICY_MAX, + _MAC_ADDRESS_POLICY_INVALID = -EINVAL, +} MACAddressPolicy; + +typedef struct Link { + int ifindex; + const char *ifname; + const char *new_name; + char **altnames; + + LinkConfig *config; + sd_device *device; + sd_device_action_t action; + + char *kind; + const char *driver; + uint16_t iftype; + uint32_t flags; + struct hw_addr_data hw_addr; + struct hw_addr_data permanent_hw_addr; + unsigned name_assign_type; + unsigned addr_assign_type; +} Link; + +struct LinkConfig { + char *filename; + char **dropins; + + NetMatch match; + LIST_HEAD(Condition, conditions); + + char *description; + struct hw_addr_data hw_addr; + MACAddressPolicy mac_address_policy; + NamePolicy *name_policy; + NamePolicy *alternative_names_policy; + char *name; + char **alternative_names; + char *alias; + uint32_t txqueues; + uint32_t rxqueues; + uint32_t txqueuelen; + uint32_t mtu; + uint32_t gso_max_segments; + size_t gso_max_size; + uint64_t speed; + Duplex duplex; + int autonegotiation; + uint32_t advertise[N_ADVERTISE]; + uint32_t wol; + char *wol_password_file; + uint8_t *wol_password; + NetDevPort port; + int features[_NET_DEV_FEAT_MAX]; + netdev_channels channels; + netdev_ring_param ring; + int rx_flow_control; + int tx_flow_control; + int autoneg_flow_control; + netdev_coalesce_param coalesce; + uint8_t mdi; + + uint32_t sr_iov_num_vfs; + OrderedHashmap *sr_iov_by_section; + + LIST_FIELDS(LinkConfig, configs); +}; + +int link_config_ctx_new(LinkConfigContext **ret); +LinkConfigContext* link_config_ctx_free(LinkConfigContext *ctx); +DEFINE_TRIVIAL_CLEANUP_FUNC(LinkConfigContext*, link_config_ctx_free); + +int link_load_one(LinkConfigContext *ctx, const char *filename); +int link_config_load(LinkConfigContext *ctx); +bool link_config_should_reload(LinkConfigContext *ctx); + +int link_new(LinkConfigContext *ctx, sd_netlink **rtnl, sd_device *device, Link **ret); +Link *link_free(Link *link); +DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free); + +int link_get_config(LinkConfigContext *ctx, Link *link); +int link_apply_config(LinkConfigContext *ctx, sd_netlink **rtnl, Link *link); + +const char *mac_address_policy_to_string(MACAddressPolicy p) _const_; +MACAddressPolicy mac_address_policy_from_string(const char *p) _pure_; + +/* gperf lookup function */ +const struct ConfigPerfItem* link_config_gperf_lookup(const char *key, GPERF_LEN_TYPE length); + +CONFIG_PARSER_PROTOTYPE(config_parse_ifalias); +CONFIG_PARSER_PROTOTYPE(config_parse_rx_tx_queues); +CONFIG_PARSER_PROTOTYPE(config_parse_txqueuelen); +CONFIG_PARSER_PROTOTYPE(config_parse_wol_password); +CONFIG_PARSER_PROTOTYPE(config_parse_mac_address_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_name_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_alternative_names_policy); diff --git a/src/udev/net/test-link-config-tables.c b/src/udev/net/test-link-config-tables.c new file mode 100644 index 0000000..a433232 --- /dev/null +++ b/src/udev/net/test-link-config-tables.c @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "link-config.h" +#include "test-tables.h" +#include "tests.h" + +int main(int argc, char **argv) { + test_setup_logging(LOG_DEBUG); + + test_table(mac_address_policy, MAC_ADDRESS_POLICY); + + return EXIT_SUCCESS; +} diff --git a/src/udev/scsi_id/README b/src/udev/scsi_id/README new file mode 100644 index 0000000..9cfe739 --- /dev/null +++ b/src/udev/scsi_id/README @@ -0,0 +1,4 @@ +scsi_id - generate a SCSI unique identifier for a given SCSI device + +Please send questions, comments or patches to <patmans@us.ibm.com> or +<linux-hotplug-devel@lists.sourceforge.net>. diff --git a/src/udev/scsi_id/scsi.h b/src/udev/scsi_id/scsi.h new file mode 100644 index 0000000..ee3e401 --- /dev/null +++ b/src/udev/scsi_id/scsi.h @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * scsi.h + * + * General scsi and linux scsi specific defines and structs. + * + * Copyright (C) IBM Corp. 2003 + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation version 2 of the License. + */ + +#include <scsi/scsi.h> + +struct scsi_ioctl_command { + unsigned inlen; /* excluding scsi command length */ + unsigned outlen; + unsigned char data[1]; + /* on input, scsi command starts here then opt. data */ +}; + +/* + * Default 5 second timeout + */ +#define DEF_TIMEOUT 5000 + +#define SENSE_BUFF_LEN 32 + +/* + * The request buffer size passed to the SCSI INQUIRY commands, use 254, + * as this is a nice value for some devices, especially some of the usb + * mass storage devices. + */ +#define SCSI_INQ_BUFF_LEN 254 + +/* + * SCSI INQUIRY vendor and model (really product) lengths. + */ +#define VENDOR_LENGTH 8 +#define MODEL_LENGTH 16 + +#define INQUIRY_CMD 0x12 +#define INQUIRY_CMDLEN 6 + +/* + * INQUIRY VPD page 0x83 identifier descriptor related values. Reference the + * SCSI Primary Commands specification for details. + */ + +/* + * id type values of id descriptors. These are assumed to fit in 4 bits. + */ +#define SCSI_ID_VENDOR_SPECIFIC 0 +#define SCSI_ID_T10_VENDOR 1 +#define SCSI_ID_EUI_64 2 +#define SCSI_ID_NAA 3 +#define SCSI_ID_RELPORT 4 +#define SCSI_ID_TGTGROUP 5 +#define SCSI_ID_LUNGROUP 6 +#define SCSI_ID_MD5 7 +#define SCSI_ID_NAME 8 + +/* + * Supported NAA values. These fit in 4 bits, so the "don't care" value + * cannot conflict with real values. + */ +#define SCSI_ID_NAA_DONT_CARE 0xff +#define SCSI_ID_NAA_IEEE_REG 0x05 +#define SCSI_ID_NAA_IEEE_REG_EXTENDED 0x06 + +/* + * Supported Code Set values. + */ +#define SCSI_ID_BINARY 1 +#define SCSI_ID_ASCII 2 + +struct scsi_id_search_values { + u_char id_type; + u_char naa_type; + u_char code_set; +}; + +/* + * Following are the "true" SCSI status codes. Linux has traditionally + * used a 1 bit right and masked version of these. So now CHECK_CONDITION + * and friends (in <scsi/scsi.h>) are deprecated. + */ +#define SCSI_CHECK_CONDITION 0x02 +#define SCSI_CONDITION_MET 0x04 +#define SCSI_BUSY 0x08 +#define SCSI_IMMEDIATE 0x10 +#define SCSI_IMMEDIATE_CONDITION_MET 0x14 +#define SCSI_RESERVATION_CONFLICT 0x18 +#define SCSI_COMMAND_TERMINATED 0x22 +#define SCSI_TASK_SET_FULL 0x28 +#define SCSI_ACA_ACTIVE 0x30 +#define SCSI_TASK_ABORTED 0x40 diff --git a/src/udev/scsi_id/scsi_id.c b/src/udev/scsi_id/scsi_id.c new file mode 100644 index 0000000..6308c52 --- /dev/null +++ b/src/udev/scsi_id/scsi_id.c @@ -0,0 +1,515 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © IBM Corp. 2003 + * Copyright © SUSE Linux Products GmbH, 2006 + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "build.h" +#include "device-nodes.h" +#include "extract-word.h" +#include "fd-util.h" +#include "fileio.h" +#include "parse-util.h" +#include "scsi_id.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "udev-util.h" + +static const struct option options[] = { + { "device", required_argument, NULL, 'd' }, + { "config", required_argument, NULL, 'f' }, + { "page", required_argument, NULL, 'p' }, + { "denylisted", no_argument, NULL, 'b' }, + { "allowlisted", no_argument, NULL, 'g' }, + { "blacklisted", no_argument, NULL, 'b' }, /* backward compat */ + { "whitelisted", no_argument, NULL, 'g' }, /* backward compat */ + { "replace-whitespace", no_argument, NULL, 'u' }, + { "sg-version", required_argument, NULL, 's' }, + { "verbose", no_argument, NULL, 'v' }, + { "version", no_argument, NULL, 'V' }, /* don't advertise -V */ + { "export", no_argument, NULL, 'x' }, + { "help", no_argument, NULL, 'h' }, + {} +}; + +static bool all_good = false; +static bool dev_specified = false; +static char config_file[MAX_PATH_LEN] = "/etc/scsi_id.config"; +static enum page_code default_page_code = PAGE_UNSPECIFIED; +static int sg_version = 4; +static bool reformat_serial = false; +static bool export = false; +static char vendor_str[64]; +static char model_str[64]; +static char vendor_enc_str[256]; +static char model_enc_str[256]; +static char revision_str[16]; +static char type_str[16]; + +static void set_type(unsigned type_num, char *to, size_t len) { + const char *type; + + switch (type_num) { + case 0: + type = "disk"; + break; + case 1: + type = "tape"; + break; + case 4: + type = "optical"; + break; + case 5: + type = "cd"; + break; + case 7: + type = "optical"; + break; + case 0xe: + type = "disk"; + break; + case 0xf: + type = "optical"; + break; + default: + type = "generic"; + break; + } + strscpy(to, len, type); +} + +/* + * get_file_options: + * + * If vendor == NULL, find a line in the config file with only "OPTIONS="; + * if vendor and model are set find the first OPTIONS line in the config + * file that matches. Set argc and argv to match the OPTIONS string. + * + * vendor and model can end in '\n'. + */ +static int get_file_options(const char *vendor, const char *model, + int *argc, char ***newargv) { + _cleanup_free_ char *vendor_in = NULL, *model_in = NULL, *options_in = NULL; /* read in from file */ + _cleanup_strv_free_ char **options_argv = NULL; + _cleanup_fclose_ FILE *f = NULL; + int lineno, r; + + f = fopen(config_file, "re"); + if (!f) { + if (errno == ENOENT) + return 1; + else { + log_error_errno(errno, "can't open %s: %m", config_file); + return -1; + } + } + + *newargv = NULL; + lineno = 0; + for (;;) { + _cleanup_free_ char *buffer = NULL, *key = NULL, *value = NULL; + const char *buf; + + vendor_in = model_in = options_in = NULL; + + r = read_line(f, MAX_BUFFER_LEN, &buffer); + if (r < 0) + return log_error_errno(r, "read_line() on line %d of %s failed: %m", lineno, config_file); + if (r == 0) + break; + buf = buffer; + lineno++; + + while (isspace(*buf)) + buf++; + + /* blank or all whitespace line */ + if (*buf == '\0') + continue; + + /* comment line */ + if (*buf == '#') + continue; + + r = extract_many_words(&buf, "=\",\n", 0, &key, &value, NULL); + if (r < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer); + + if (strcaseeq(key, "VENDOR")) { + vendor_in = TAKE_PTR(value); + + key = mfree(key); + r = extract_many_words(&buf, "=\",\n", 0, &key, &value, NULL); + if (r < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer); + + if (strcaseeq(key, "MODEL")) { + model_in = TAKE_PTR(value); + + key = mfree(key); + r = extract_many_words(&buf, "=\",\n", 0, &key, &value, NULL); + if (r < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer); + } + } + + if (strcaseeq(key, "OPTIONS")) + options_in = TAKE_PTR(value); + + /* + * Only allow: [vendor=foo[,model=bar]]options=stuff + */ + if (!options_in || (!vendor_in && model_in)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer); + if (!vendor) { + if (!vendor_in) + break; + } else if (vendor_in && + startswith(vendor, vendor_in) && + (!model_in || startswith(model, model_in))) { + /* + * Matched vendor and optionally model. + * + * Note: a short vendor_in or model_in can + * give a partial match (that is FOO + * matches FOOBAR). + */ + break; + } + + vendor_in = mfree(vendor_in); + model_in = mfree(model_in); + options_in = mfree(options_in); + + } + + if (vendor_in == NULL && model_in == NULL && options_in == NULL) + return 1; /* No matches */ + + /* + * Something matched. Allocate newargv, and store + * values found in options_in. + */ + options_argv = strv_split(options_in, " \t"); + if (!options_argv) + return log_oom(); + r = strv_prepend(&options_argv, ""); /* getopt skips over argv[0] */ + if (r < 0) + return r; + *newargv = TAKE_PTR(options_argv); + *argc = strv_length(*newargv); + + return 0; +} + +static void help(void) { + printf("Usage: %s [OPTION...] DEVICE\n\n" + "SCSI device identification.\n\n" + " -h --help Print this message\n" + " --version Print version of the program\n\n" + " -d --device= Device node for SG_IO commands\n" + " -f --config= Location of config file\n" + " -p --page=0x80|0x83|pre-spc3-83 SCSI page (0x80, 0x83, pre-spc3-83)\n" + " -s --sg-version=3|4 Use SGv3 or SGv4\n" + " -b --denylisted Treat device as denylisted\n" + " -g --allowlisted Treat device as allowlisted\n" + " -u --replace-whitespace Replace all whitespace by underscores\n" + " -v --verbose Verbose logging\n" + " -x --export Print values as environment keys\n", + program_invocation_short_name); +} + +static int set_options(int argc, char **argv, + char *maj_min_dev) { + int option; + + /* + * optind is a global extern used by getopt. Since we can call + * set_options twice (once for command line, and once for config + * file) we have to reset this back to 1. + */ + optind = 1; + while ((option = getopt_long(argc, argv, "d:f:gp:uvVxhbs:", options, NULL)) >= 0) + switch (option) { + case 'b': + all_good = false; + break; + + case 'd': + dev_specified = true; + strscpy(maj_min_dev, MAX_PATH_LEN, optarg); + break; + + case 'f': + strscpy(config_file, MAX_PATH_LEN, optarg); + break; + + case 'g': + all_good = true; + break; + + case 'h': + help(); + exit(EXIT_SUCCESS); + + case 'p': + if (streq(optarg, "0x80")) + default_page_code = PAGE_80; + else if (streq(optarg, "0x83")) + default_page_code = PAGE_83; + else if (streq(optarg, "pre-spc3-83")) + default_page_code = PAGE_83_PRE_SPC3; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown page code '%s'", + optarg); + break; + + case 's': + sg_version = atoi(optarg); + if (sg_version < 3 || sg_version > 4) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown SG version '%s'", + optarg); + break; + + case 'u': + reformat_serial = true; + break; + + case 'v': + log_set_target(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_DEBUG); + log_open(); + break; + + case 'V': + version(); + exit(EXIT_SUCCESS); + + case 'x': + export = true; + break; + + case '?': + return -1; + + default: + assert_not_reached(); + } + + if (optind < argc && !dev_specified) { + dev_specified = true; + strscpy(maj_min_dev, MAX_PATH_LEN, argv[optind]); + } + + return 0; +} + +static int per_dev_options(struct scsi_id_device *dev_scsi, int *good_bad, int *page_code) { + _cleanup_strv_free_ char **newargv = NULL; + int retval; + int newargc; + int option; + + *good_bad = all_good; + *page_code = default_page_code; + + retval = get_file_options(vendor_str, model_str, &newargc, &newargv); + + optind = 1; /* reset this global extern */ + while (retval == 0) { + option = getopt_long(newargc, newargv, "bgp:", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'b': + *good_bad = 0; + break; + + case 'g': + *good_bad = 1; + break; + + case 'p': + if (streq(optarg, "0x80")) { + *page_code = PAGE_80; + } else if (streq(optarg, "0x83")) { + *page_code = PAGE_83; + } else if (streq(optarg, "pre-spc3-83")) { + *page_code = PAGE_83_PRE_SPC3; + } else { + log_error("Unknown page code '%s'", optarg); + retval = -1; + } + break; + + default: + log_error("Unknown or bad option '%c' (0x%x)", option, (unsigned) option); + retval = -1; + break; + } + } + + return retval; +} + +static int set_inq_values(struct scsi_id_device *dev_scsi, const char *path) { + int retval; + + dev_scsi->use_sg = sg_version; + + retval = scsi_std_inquiry(dev_scsi, path); + if (retval) + return retval; + + encode_devnode_name(dev_scsi->vendor, vendor_enc_str, sizeof(vendor_enc_str)); + encode_devnode_name(dev_scsi->model, model_enc_str, sizeof(model_enc_str)); + + udev_replace_whitespace(dev_scsi->vendor, vendor_str, sizeof(vendor_str)-1); + udev_replace_chars(vendor_str, NULL); + udev_replace_whitespace(dev_scsi->model, model_str, sizeof(model_str)-1); + udev_replace_chars(model_str, NULL); + set_type(dev_scsi->type, type_str, sizeof(type_str)); + udev_replace_whitespace(dev_scsi->revision, revision_str, sizeof(revision_str)-1); + udev_replace_chars(revision_str, NULL); + return 0; +} + +/* + * scsi_id: try to get an id, if one is found, printf it to stdout. + * returns a value passed to exit() - 0 if printed an id, else 1. + */ +static int scsi_id(char *maj_min_dev) { + struct scsi_id_device dev_scsi = {}; + int good_dev; + int page_code; + int retval = 0; + + if (set_inq_values(&dev_scsi, maj_min_dev) < 0) { + retval = 1; + goto out; + } + + /* get per device (vendor + model) options from the config file */ + per_dev_options(&dev_scsi, &good_dev, &page_code); + if (!good_dev) { + retval = 1; + goto out; + } + + /* read serial number from mode pages (no values for optical drives) */ + scsi_get_serial(&dev_scsi, maj_min_dev, page_code, MAX_SERIAL_LEN); + + if (export) { + char serial_str[MAX_SERIAL_LEN]; + + printf("ID_SCSI=1\n"); + printf("ID_VENDOR=%s\n", vendor_str); + printf("ID_VENDOR_ENC=%s\n", vendor_enc_str); + printf("ID_MODEL=%s\n", model_str); + printf("ID_MODEL_ENC=%s\n", model_enc_str); + printf("ID_REVISION=%s\n", revision_str); + printf("ID_TYPE=%s\n", type_str); + if (dev_scsi.serial[0] != '\0') { + udev_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1); + udev_replace_chars(serial_str, NULL); + printf("ID_SERIAL=%s\n", serial_str); + udev_replace_whitespace(dev_scsi.serial_short, serial_str, sizeof(serial_str)-1); + udev_replace_chars(serial_str, NULL); + printf("ID_SERIAL_SHORT=%s\n", serial_str); + } + if (dev_scsi.wwn[0] != '\0') { + printf("ID_WWN=0x%s\n", dev_scsi.wwn); + if (dev_scsi.wwn_vendor_extension[0] != '\0') { + printf("ID_WWN_VENDOR_EXTENSION=0x%s\n", dev_scsi.wwn_vendor_extension); + printf("ID_WWN_WITH_EXTENSION=0x%s%s\n", dev_scsi.wwn, dev_scsi.wwn_vendor_extension); + } else + printf("ID_WWN_WITH_EXTENSION=0x%s\n", dev_scsi.wwn); + } + if (dev_scsi.tgpt_group[0] != '\0') + printf("ID_TARGET_PORT=%s\n", dev_scsi.tgpt_group); + if (dev_scsi.unit_serial_number[0] != '\0') + printf("ID_SCSI_SERIAL=%s\n", dev_scsi.unit_serial_number); + goto out; + } + + if (dev_scsi.serial[0] == '\0') { + retval = 1; + goto out; + } + + if (reformat_serial) { + char serial_str[MAX_SERIAL_LEN]; + + udev_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1); + udev_replace_chars(serial_str, NULL); + printf("%s\n", serial_str); + goto out; + } + + printf("%s\n", dev_scsi.serial); +out: + return retval; +} + +int main(int argc, char **argv) { + _cleanup_strv_free_ char **newargv = NULL; + int retval = 0; + char maj_min_dev[MAX_PATH_LEN]; + int newargc; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + /* + * Get config file options. + */ + retval = get_file_options(NULL, NULL, &newargc, &newargv); + if (retval < 0) { + retval = 1; + goto exit; + } + if (retval == 0) { + assert(newargv); + + if (set_options(newargc, newargv, maj_min_dev) < 0) { + retval = 2; + goto exit; + } + } + + /* + * Get command line options (overriding any config file settings). + */ + if (set_options(argc, argv, maj_min_dev) < 0) + exit(EXIT_FAILURE); + + if (!dev_specified) { + log_error("No device specified."); + retval = 1; + goto exit; + } + + retval = scsi_id(maj_min_dev); + +exit: + log_close(); + return retval; +} diff --git a/src/udev/scsi_id/scsi_id.h b/src/udev/scsi_id/scsi_id.h new file mode 100644 index 0000000..9ab3341 --- /dev/null +++ b/src/udev/scsi_id/scsi_id.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * Copyright © IBM Corp. 2003 + */ + +#define MAX_PATH_LEN 512 + +/* + * MAX_ATTR_LEN: maximum length of the result of reading a sysfs + * attribute. + */ +#define MAX_ATTR_LEN 256 + +/* + * MAX_SERIAL_LEN: the maximum length of the serial number, including + * added prefixes such as vendor and product (model) strings. + */ +#define MAX_SERIAL_LEN 256 + +/* + * MAX_BUFFER_LEN: maximum buffer size and line length used while reading + * the config file. + */ +#define MAX_BUFFER_LEN 256 + +struct scsi_id_device { + char vendor[9]; + char model[17]; + char revision[5]; + char kernel[64]; + char serial[MAX_SERIAL_LEN]; + char serial_short[MAX_SERIAL_LEN]; + unsigned type; + int use_sg; + + /* Always from page 0x80 e.g. 'B3G1P8500RWT' - may not be unique */ + char unit_serial_number[MAX_SERIAL_LEN]; + + /* NULs if not set - otherwise hex encoding using lower-case e.g. '50014ee0016eb572' */ + char wwn[17]; + + /* NULs if not set - otherwise hex encoding using lower-case e.g. '0xe00000d80000' */ + char wwn_vendor_extension[17]; + + /* NULs if not set - otherwise decimal number */ + char tgpt_group[8]; +}; + +int scsi_std_inquiry(struct scsi_id_device *dev_scsi, const char *devname); +int scsi_get_serial(struct scsi_id_device *dev_scsi, const char *devname, + int page_code, int len); + +/* + * Page code values. + */ +enum page_code { + PAGE_83_PRE_SPC3 = -0x83, + PAGE_UNSPECIFIED = 0x00, + PAGE_80 = 0x80, + PAGE_83 = 0x83, +}; diff --git a/src/udev/scsi_id/scsi_serial.c b/src/udev/scsi_id/scsi_serial.c new file mode 100644 index 0000000..aed6082 --- /dev/null +++ b/src/udev/scsi_id/scsi_serial.c @@ -0,0 +1,892 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © IBM Corp. 2003 + * + * Author: Patrick Mansfield<patmans@us.ibm.com> + */ + +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <linux/bsg.h> +#include <linux/types.h> +#include <scsi/scsi.h> +#include <scsi/sg.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include "devnum-util.h" +#include "memory-util.h" +#include "random-util.h" +#include "scsi.h" +#include "scsi_id.h" +#include "string-util.h" + +/* + * A priority based list of id, naa, and binary/ascii for the identifier + * descriptor in VPD page 0x83. + * + * Brute force search for a match starting with the first value in the + * following id_search_list. This is not a performance issue, since there + * is normally one or some small number of descriptors. + */ +static const struct scsi_id_search_values id_search_list[] = { + { SCSI_ID_TGTGROUP, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG_EXTENDED, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG_EXTENDED, SCSI_ID_ASCII }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG, SCSI_ID_ASCII }, + /* + * Devices already exist using NAA values that are now marked + * reserved. These should not conflict with other values, or it is + * a bug in the device. As long as we find the IEEE extended one + * first, we really don't care what other ones are used. Using + * don't care here means that a device that returns multiple + * non-IEEE descriptors in a random order will get different + * names. + */ + { SCSI_ID_NAA, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, + { SCSI_ID_EUI_64, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_EUI_64, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, + { SCSI_ID_T10_VENDOR, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_T10_VENDOR, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, + { SCSI_ID_VENDOR_SPECIFIC, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_VENDOR_SPECIFIC, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, +}; + +static const char hex_str[]="0123456789abcdef"; + +/* + * Values returned in the result/status, only the ones used by the code + * are used here. + */ + +#define DID_NO_CONNECT 0x01 /* Unable to connect before timeout */ +#define DID_BUS_BUSY 0x02 /* Bus remain busy until timeout */ +#define DID_TIME_OUT 0x03 /* Timed out for some other reason */ +#define DID_TRANSPORT_DISRUPTED 0x0e /* Transport disrupted and should retry */ +#define DRIVER_TIMEOUT 0x06 +#define DRIVER_SENSE 0x08 /* Sense_buffer has been set */ + +/* The following "category" function returns one of the following */ +#define SG_ERR_CAT_CLEAN 0 /* No errors or other information */ +#define SG_ERR_CAT_MEDIA_CHANGED 1 /* interpreted from sense buffer */ +#define SG_ERR_CAT_RESET 2 /* interpreted from sense buffer */ +#define SG_ERR_CAT_TIMEOUT 3 +#define SG_ERR_CAT_RECOVERED 4 /* Successful command after recovered err */ +#define SG_ERR_CAT_NOTSUPPORTED 5 /* Illegal / unsupported command */ +#define SG_ERR_CAT_RETRY 6 /* Command should be retried */ +#define SG_ERR_CAT_SENSE 98 /* Something else in the sense buffer */ +#define SG_ERR_CAT_OTHER 99 /* Some other error/warning */ + +static int do_scsi_page80_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int max_len); + +static int sg_err_category_new(int scsi_status, int msg_status, int + host_status, int driver_status, const + unsigned char *sense_buffer, int sb_len) { + scsi_status &= 0x7e; + + /* + * XXX change to return only two values - failed or OK. + */ + + if (!scsi_status && !host_status && !driver_status) + return SG_ERR_CAT_CLEAN; + + if (IN_SET(scsi_status, SCSI_CHECK_CONDITION, SCSI_COMMAND_TERMINATED) || + (driver_status & 0xf) == DRIVER_SENSE) { + if (sense_buffer && (sb_len > 2)) { + int sense_key; + unsigned char asc; + + if (sense_buffer[0] & 0x2) { + sense_key = sense_buffer[1] & 0xf; + asc = sense_buffer[2]; + } else { + sense_key = sense_buffer[2] & 0xf; + asc = (sb_len > 12) ? sense_buffer[12] : 0; + } + + if (sense_key == RECOVERED_ERROR) + return SG_ERR_CAT_RECOVERED; + else if (sense_key == UNIT_ATTENTION) { + if (0x28 == asc) + return SG_ERR_CAT_MEDIA_CHANGED; + if (0x29 == asc) + return SG_ERR_CAT_RESET; + } else if (sense_key == ILLEGAL_REQUEST) + return SG_ERR_CAT_NOTSUPPORTED; + } + return SG_ERR_CAT_SENSE; + } + if (host_status) { + if (IN_SET(host_status, DID_NO_CONNECT, DID_BUS_BUSY, DID_TIME_OUT)) + return SG_ERR_CAT_TIMEOUT; + if (host_status == DID_TRANSPORT_DISRUPTED) + return SG_ERR_CAT_RETRY; + } + if (driver_status) { + if (driver_status == DRIVER_TIMEOUT) + return SG_ERR_CAT_TIMEOUT; + } + return SG_ERR_CAT_OTHER; +} + +static int sg_err_category3(struct sg_io_hdr *hp) { + return sg_err_category_new(hp->status, hp->msg_status, + hp->host_status, hp->driver_status, + hp->sbp, hp->sb_len_wr); +} + +static int sg_err_category4(struct sg_io_v4 *hp) { + return sg_err_category_new(hp->device_status, 0, + hp->transport_status, hp->driver_status, + (unsigned char *)(uintptr_t)hp->response, + hp->response_len); +} + +static int scsi_dump_sense(struct scsi_id_device *dev_scsi, + unsigned char *sense_buffer, int sb_len) { + int s; + unsigned code, sense_class, sense_key, asc, ascq; + + /* + * Figure out and print the sense key, asc and ascq. + * + * If you want to suppress these for a particular drive model, add + * a deny list entry in the scsi_id config file. + * + * XXX We probably need to: lookup the sense/asc/ascq in a retry + * table, and if found return 1 (after dumping the sense, asc, and + * ascq). So, if/when we get something like a power on/reset, + * we'll retry the command. + */ + + if (sb_len < 1) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense buffer empty", + dev_scsi->kernel); + + sense_class = (sense_buffer[0] >> 4) & 0x07; + code = sense_buffer[0] & 0xf; + + if (sense_class == 7) { + /* + * extended sense data. + */ + s = sense_buffer[7] + 8; + if (sb_len < s) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense buffer too small %d bytes, %d bytes too short", + dev_scsi->kernel, sb_len, + s - sb_len); + + if (IN_SET(code, 0x0, 0x1)) { + sense_key = sense_buffer[2] & 0xf; + if (s < 14) + /* + * Possible? + */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense result too small %d bytes", + dev_scsi->kernel, s); + + asc = sense_buffer[12]; + ascq = sense_buffer[13]; + } else if (IN_SET(code, 0x2, 0x3)) { + sense_key = sense_buffer[1] & 0xf; + asc = sense_buffer[2]; + ascq = sense_buffer[3]; + } else + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: invalid sense code 0x%x", + dev_scsi->kernel, code); + + log_debug("%s: sense key 0x%x ASC 0x%x ASCQ 0x%x", + dev_scsi->kernel, sense_key, asc, ascq); + } else { + if (sb_len < 4) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense buffer too small %d bytes, %d bytes too short", + dev_scsi->kernel, sb_len, + 4 - sb_len); + + if (sense_buffer[0] < 15) + log_debug("%s: old sense key: 0x%x", dev_scsi->kernel, sense_buffer[0] & 0x0fu); + else + log_debug("%s: sense = %2x %2x", + dev_scsi->kernel, sense_buffer[0], sense_buffer[2]); + log_debug("%s: non-extended sense class %u code 0x%0x", + dev_scsi->kernel, sense_class, code); + + } + + return -1; +} + +static int scsi_dump(struct scsi_id_device *dev_scsi, struct sg_io_hdr *io) { + if (!io->status && !io->host_status && !io->msg_status && + !io->driver_status) + /* + * Impossible, should not be called. + */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: called with no error", + __func__); + + log_debug("%s: sg_io failed status 0x%x 0x%x 0x%x 0x%x", + dev_scsi->kernel, io->driver_status, io->host_status, io->msg_status, io->status); + if (io->status == SCSI_CHECK_CONDITION) + return scsi_dump_sense(dev_scsi, io->sbp, io->sb_len_wr); + else + return -1; +} + +static int scsi_dump_v4(struct scsi_id_device *dev_scsi, struct sg_io_v4 *io) { + if (!io->device_status && !io->transport_status && + !io->driver_status) + /* + * Impossible, should not be called. + */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: called with no error", + __func__); + + log_debug("%s: sg_io failed status 0x%x 0x%x 0x%x", + dev_scsi->kernel, io->driver_status, io->transport_status, io->device_status); + if (io->device_status == SCSI_CHECK_CONDITION) + return scsi_dump_sense(dev_scsi, (unsigned char *)(uintptr_t)io->response, + io->response_len); + else + return -1; +} + +static int scsi_inquiry(struct scsi_id_device *dev_scsi, int fd, + unsigned char evpd, unsigned char page, + unsigned char *buf, unsigned buflen) { + unsigned char inq_cmd[INQUIRY_CMDLEN] = + { INQUIRY_CMD, evpd, page, 0, buflen, 0 }; + unsigned char sense[SENSE_BUFF_LEN]; + void *io_buf; + struct sg_io_v4 io_v4; + struct sg_io_hdr io_hdr; + int retry = 3; /* rather random */ + int retval; + + if (buflen > SCSI_INQ_BUFF_LEN) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "buflen %u too long", buflen); + +resend: + if (dev_scsi->use_sg == 4) { + memzero(&io_v4, sizeof(struct sg_io_v4)); + io_v4.guard = 'Q'; + io_v4.protocol = BSG_PROTOCOL_SCSI; + io_v4.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD; + io_v4.request_len = sizeof(inq_cmd); + io_v4.request = (uintptr_t)inq_cmd; + io_v4.max_response_len = sizeof(sense); + io_v4.response = (uintptr_t)sense; + io_v4.din_xfer_len = buflen; + io_v4.din_xferp = (uintptr_t)buf; + io_buf = (void *)&io_v4; + } else { + memzero(&io_hdr, sizeof(struct sg_io_hdr)); + io_hdr.interface_id = 'S'; + io_hdr.cmd_len = sizeof(inq_cmd); + io_hdr.mx_sb_len = sizeof(sense); + io_hdr.dxfer_direction = SG_DXFER_FROM_DEV; + io_hdr.dxfer_len = buflen; + io_hdr.dxferp = buf; + io_hdr.cmdp = inq_cmd; + io_hdr.sbp = sense; + io_hdr.timeout = DEF_TIMEOUT; + io_buf = (void *)&io_hdr; + } + + retval = ioctl(fd, SG_IO, io_buf); + if (retval < 0) { + if (IN_SET(errno, EINVAL, ENOSYS) && dev_scsi->use_sg == 4) { + dev_scsi->use_sg = 3; + goto resend; + } + log_debug_errno(errno, "%s: ioctl failed: %m", dev_scsi->kernel); + goto error; + } + + if (dev_scsi->use_sg == 4) + retval = sg_err_category4(io_buf); + else + retval = sg_err_category3(io_buf); + + switch (retval) { + case SG_ERR_CAT_NOTSUPPORTED: + buf[1] = 0; + _fallthrough_; + case SG_ERR_CAT_CLEAN: + case SG_ERR_CAT_RECOVERED: + retval = 0; + break; + case SG_ERR_CAT_RETRY: + break; + + default: + if (dev_scsi->use_sg == 4) + retval = scsi_dump_v4(dev_scsi, io_buf); + else + retval = scsi_dump(dev_scsi, io_buf); + } + + if (!retval) { + retval = buflen; + } else if (retval > 0) { + if (--retry > 0) + goto resend; + retval = -1; + } + +error: + if (retval < 0) + log_debug("%s: Unable to get INQUIRY vpd %d page 0x%x.", + dev_scsi->kernel, evpd, page); + + return retval; +} + +/* Get list of supported EVPD pages */ +static int do_scsi_page0_inquiry(struct scsi_id_device *dev_scsi, int fd, + unsigned char *buffer, unsigned len) { + int retval; + + memzero(buffer, len); + retval = scsi_inquiry(dev_scsi, fd, 1, 0x0, buffer, len); + if (retval < 0) + return 1; + + if (buffer[1] != 0) { + log_debug("%s: page 0 not available.", dev_scsi->kernel); + return 1; + } + if (buffer[3] > len) { + log_debug("%s: page 0 buffer too long %d", dev_scsi->kernel, buffer[3]); + return 1; + } + + /* + * Following check is based on code once included in the 2.5.x + * kernel. + * + * Some ill behaved devices return the standard inquiry here + * rather than the evpd data, snoop the data to verify. + */ + if (buffer[3] > MODEL_LENGTH) { + /* + * If the vendor id appears in the page assume the page is + * invalid. + */ + if (strneq((char*) buffer + VENDOR_LENGTH, dev_scsi->vendor, VENDOR_LENGTH)) { + log_debug("%s: invalid page0 data", dev_scsi->kernel); + return 1; + } + } + return 0; +} + +static int append_vendor_model( + const struct scsi_id_device *dev_scsi, + char buf[static VENDOR_LENGTH + MODEL_LENGTH]) { + + assert(dev_scsi); + assert(buf); + + if (strnlen(dev_scsi->vendor, VENDOR_LENGTH) != VENDOR_LENGTH) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: bad vendor string \"%s\"", + dev_scsi->kernel, dev_scsi->vendor); + if (strnlen(dev_scsi->model, MODEL_LENGTH) != MODEL_LENGTH) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: bad model string \"%s\"", + dev_scsi->kernel, dev_scsi->model); + memcpy(buf, dev_scsi->vendor, VENDOR_LENGTH); + memcpy(buf + VENDOR_LENGTH, dev_scsi->model, MODEL_LENGTH); + return VENDOR_LENGTH + MODEL_LENGTH; +} + +/* + * check_fill_0x83_id - check the page 0x83 id, if OK allocate and fill + * serial number. + */ +static int check_fill_0x83_id(struct scsi_id_device *dev_scsi, + unsigned char *page_83, + const struct scsi_id_search_values + *id_search, char *serial, char *serial_short, + int max_len, char *wwn, + char *wwn_vendor_extension, char *tgpt_group) { + int i, j, s, len; + + /* + * ASSOCIATION must be with the device (value 0) + * or with the target port for SCSI_ID_TGTPORT + */ + if ((page_83[1] & 0x30) == 0x10) { + if (id_search->id_type != SCSI_ID_TGTGROUP) + return 1; + } else if ((page_83[1] & 0x30) != 0) + return 1; + + if ((page_83[1] & 0x0f) != id_search->id_type) + return 1; + + /* + * Possibly check NAA sub-type. + */ + if ((id_search->naa_type != SCSI_ID_NAA_DONT_CARE) && + (id_search->naa_type != (page_83[4] & 0xf0) >> 4)) + return 1; + + /* + * Check for matching code set - ASCII or BINARY. + */ + if ((page_83[0] & 0x0f) != id_search->code_set) + return 1; + + /* + * page_83[3]: identifier length + */ + len = page_83[3]; + if ((page_83[0] & 0x0f) != SCSI_ID_ASCII) + /* + * If not ASCII, use two bytes for each binary value. + */ + len *= 2; + + /* + * Add one byte for the NUL termination, and one for the id_type. + */ + len += 2; + if (id_search->id_type == SCSI_ID_VENDOR_SPECIFIC) + len += VENDOR_LENGTH + MODEL_LENGTH; + + if (max_len < len) { + log_debug("%s: length %d too short - need %d", + dev_scsi->kernel, max_len, len); + return 1; + } + + if (id_search->id_type == SCSI_ID_TGTGROUP && tgpt_group != NULL) { + unsigned group; + + group = ((unsigned)page_83[6] << 8) | page_83[7]; + sprintf(tgpt_group,"%x", group); + return 1; + } + + serial[0] = hex_str[id_search->id_type]; + + /* + * For SCSI_ID_VENDOR_SPECIFIC prepend the vendor and model before + * the id since it is not unique across all vendors and models, + * this differs from SCSI_ID_T10_VENDOR, where the vendor is + * included in the identifier. + */ + if (id_search->id_type == SCSI_ID_VENDOR_SPECIFIC) + if (append_vendor_model(dev_scsi, serial + 1) < 0) + return 1; + + i = 4; /* offset to the start of the identifier */ + s = j = strlen(serial); + if ((page_83[0] & 0x0f) == SCSI_ID_ASCII) { + /* + * ASCII descriptor. + */ + while (i < (4 + page_83[3])) + serial[j++] = page_83[i++]; + } else { + /* + * Binary descriptor, convert to ASCII, using two bytes of + * ASCII for each byte in the page_83. + */ + while (i < (4 + page_83[3])) { + serial[j++] = hex_str[(page_83[i] & 0xf0) >> 4]; + serial[j++] = hex_str[page_83[i] & 0x0f]; + i++; + } + } + + strcpy(serial_short, serial + s); + + if (id_search->id_type == SCSI_ID_NAA && wwn != NULL) { + strncpy(wwn, serial + s, 16); + if (wwn_vendor_extension) + strncpy(wwn_vendor_extension, serial + s + 16, 16); + } + + return 0; +} + +/* Extract the raw binary from VPD 0x83 pre-SPC devices */ +static int check_fill_0x83_prespc3(struct scsi_id_device *dev_scsi, + unsigned char *page_83, + const struct scsi_id_search_values + *id_search, char *serial, char *serial_short, int max_len) { + int i, j; + + serial[0] = hex_str[SCSI_ID_NAA]; + /* serial has been memset to zero before */ + j = strlen(serial); /* j = 1; */ + + for (i = 0; (i < page_83[3]) && (j < max_len-3); ++i) { + serial[j++] = hex_str[(page_83[4+i] & 0xf0) >> 4]; + serial[j++] = hex_str[ page_83[4+i] & 0x0f]; + } + serial[max_len-1] = 0; + strncpy(serial_short, serial, max_len-1); + return 0; +} + +/* Get device identification VPD page */ +static int do_scsi_page83_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int len, + char *unit_serial_number, char *wwn, + char *wwn_vendor_extension, char *tgpt_group) { + int retval; + unsigned id_ind, j; + unsigned char page_83[SCSI_INQ_BUFF_LEN]; + + /* also pick up the page 80 serial number */ + do_scsi_page80_inquiry(dev_scsi, fd, NULL, unit_serial_number, MAX_SERIAL_LEN); + + memzero(page_83, SCSI_INQ_BUFF_LEN); + retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_83, page_83, + SCSI_INQ_BUFF_LEN); + if (retval < 0) + return 1; + + if (page_83[1] != PAGE_83) { + log_debug("%s: Invalid page 0x83", dev_scsi->kernel); + return 1; + } + + /* + * XXX Some devices (IBM 3542) return all spaces for an identifier if + * the LUN is not actually configured. This leads to identifiers of + * the form: "1 ". + */ + + /* + * Model 4, 5, and (some) model 6 EMC Symmetrix devices return + * a page 83 reply according to SCSI-2 format instead of SPC-2/3. + * + * The SCSI-2 page 83 format returns an IEEE WWN in binary + * encoded hexi-decimal in the 16 bytes following the initial + * 4-byte page 83 reply header. + * + * Both the SPC-2 and SPC-3 formats return an IEEE WWN as part + * of an Identification descriptor. The 3rd byte of the first + * Identification descriptor is a reserved (BSZ) byte field. + * + * Reference the 7th byte of the page 83 reply to determine + * whether the reply is compliant with SCSI-2 or SPC-2/3 + * specifications. A zero value in the 7th byte indicates + * an SPC-2/3 conformant reply, (i.e., the reserved field of the + * first Identification descriptor). This byte will be non-zero + * for a SCSI-2 conformant page 83 reply from these EMC + * Symmetrix models since the 7th byte of the reply corresponds + * to the 4th and 5th nibbles of the 6-byte OUI for EMC, that is, + * 0x006048. + */ + + if (page_83[6] != 0) + return check_fill_0x83_prespc3(dev_scsi, page_83, id_search_list, + serial, serial_short, len); + + /* + * Search for a match in the prioritized id_search_list - since WWN ids + * come first we can pick up the WWN in check_fill_0x83_id(). + */ + for (id_ind = 0; + id_ind < sizeof(id_search_list)/sizeof(id_search_list[0]); + id_ind++) { + /* + * Examine each descriptor returned. There is normally only + * one or a small number of descriptors. + */ + for (j = 4; j <= ((unsigned)page_83[2] << 8) + (unsigned)page_83[3] + 3; j += page_83[j + 3] + 4) { + retval = check_fill_0x83_id(dev_scsi, page_83 + j, + id_search_list + id_ind, + serial, serial_short, len, + wwn, wwn_vendor_extension, + tgpt_group); + if (!retval) + return retval; + else if (retval < 0) + return retval; + } + } + return 1; +} + +/* + * Get device identification VPD page for older SCSI-2 device which is not + * compliant with either SPC-2 or SPC-3 format. + * + * Return the hard coded error code value 2 if the page 83 reply is not + * conformant to the SCSI-2 format. + */ +static int do_scsi_page83_prespc3_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int len) { + int retval; + int i, j; + unsigned char page_83[SCSI_INQ_BUFF_LEN]; + + memzero(page_83, SCSI_INQ_BUFF_LEN); + retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_83, page_83, SCSI_INQ_BUFF_LEN); + if (retval < 0) + return 1; + + if (page_83[1] != PAGE_83) { + log_debug("%s: Invalid page 0x83", dev_scsi->kernel); + return 1; + } + /* + * Model 4, 5, and (some) model 6 EMC Symmetrix devices return + * a page 83 reply according to SCSI-2 format instead of SPC-2/3. + * + * The SCSI-2 page 83 format returns an IEEE WWN in binary + * encoded hexi-decimal in the 16 bytes following the initial + * 4-byte page 83 reply header. + * + * Both the SPC-2 and SPC-3 formats return an IEEE WWN as part + * of an Identification descriptor. The 3rd byte of the first + * Identification descriptor is a reserved (BSZ) byte field. + * + * Reference the 7th byte of the page 83 reply to determine + * whether the reply is compliant with SCSI-2 or SPC-2/3 + * specifications. A zero value in the 7th byte indicates + * an SPC-2/3 conformant reply, (i.e., the reserved field of the + * first Identification descriptor). This byte will be non-zero + * for a SCSI-2 conformant page 83 reply from these EMC + * Symmetrix models since the 7th byte of the reply corresponds + * to the 4th and 5th nibbles of the 6-byte OUI for EMC, that is, + * 0x006048. + */ + if (page_83[6] == 0) + return 2; + + serial[0] = hex_str[SCSI_ID_NAA]; + /* + * The first four bytes contain data, not a descriptor. + */ + i = 4; + j = strlen(serial); + /* + * Binary descriptor, convert to ASCII, + * using two bytes of ASCII for each byte + * in the page_83. + */ + while (i < (page_83[3]+4)) { + serial[j++] = hex_str[(page_83[i] & 0xf0) >> 4]; + serial[j++] = hex_str[page_83[i] & 0x0f]; + i++; + } + return 0; +} + +/* Get unit serial number VPD page */ +static int do_scsi_page80_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int max_len) { + int retval; + int ser_ind; + int i; + int len; + unsigned char buf[SCSI_INQ_BUFF_LEN]; + + memzero(buf, SCSI_INQ_BUFF_LEN); + retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_80, buf, SCSI_INQ_BUFF_LEN); + if (retval < 0) + return retval; + + if (buf[1] != PAGE_80) { + log_debug("%s: Invalid page 0x80", dev_scsi->kernel); + return 1; + } + + len = 1 + VENDOR_LENGTH + MODEL_LENGTH + buf[3]; + if (max_len < len) { + log_debug("%s: length %d too short - need %d", + dev_scsi->kernel, max_len, len); + return 1; + } + /* + * Prepend 'S' to avoid unlikely collision with page 0x83 vendor + * specific type where we prepend '0' + vendor + model. + */ + len = buf[3]; + if (serial) { + serial[0] = 'S'; + ser_ind = append_vendor_model(dev_scsi, serial + 1); + if (ser_ind < 0) + return 1; + ser_ind++; /* for the leading 'S' */ + for (i = 4; i < len + 4; i++, ser_ind++) + serial[ser_ind] = buf[i]; + } + if (serial_short) { + memcpy(serial_short, buf + 4, len); + serial_short[len] = '\0'; + } + return 0; +} + +int scsi_std_inquiry(struct scsi_id_device *dev_scsi, const char *devname) { + int fd; + unsigned char buf[SCSI_INQ_BUFF_LEN]; + struct stat statbuf; + int err = 0; + + fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); + if (fd < 0) { + log_debug_errno(errno, "scsi_id: cannot open %s: %m", devname); + return 1; + } + + if (fstat(fd, &statbuf) < 0) { + log_debug_errno(errno, "scsi_id: cannot stat %s: %m", devname); + err = 2; + goto out; + } + format_devnum(statbuf.st_rdev, dev_scsi->kernel); + + memzero(buf, SCSI_INQ_BUFF_LEN); + err = scsi_inquiry(dev_scsi, fd, 0, 0, buf, SCSI_INQ_BUFF_LEN); + if (err < 0) + goto out; + + err = 0; + memcpy(dev_scsi->vendor, buf + 8, 8); + dev_scsi->vendor[8] = '\0'; + memcpy(dev_scsi->model, buf + 16, 16); + dev_scsi->model[16] = '\0'; + memcpy(dev_scsi->revision, buf + 32, 4); + dev_scsi->revision[4] = '\0'; + dev_scsi->type = buf[0] & 0x1f; + +out: + close(fd); + return err; +} + +int scsi_get_serial(struct scsi_id_device *dev_scsi, const char *devname, + int page_code, int len) { + unsigned char page0[SCSI_INQ_BUFF_LEN]; + int fd = -EBADF; + int cnt; + int ind; + int retval; + + memzero(dev_scsi->serial, len); + for (cnt = 20; cnt > 0; cnt--) { + fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY); + if (fd >= 0 || errno != EBUSY) + break; + + usleep_safe(200U*USEC_PER_MSEC + random_u64_range(100U*USEC_PER_MSEC)); + } + if (fd < 0) + return 1; + + if (page_code == PAGE_80) { + if (do_scsi_page80_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len)) { + retval = 1; + goto completed; + } else { + retval = 0; + goto completed; + } + } else if (page_code == PAGE_83) { + if (do_scsi_page83_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) { + retval = 1; + goto completed; + } else { + retval = 0; + goto completed; + } + } else if (page_code == PAGE_83_PRE_SPC3) { + retval = do_scsi_page83_prespc3_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len); + if (retval) { + /* + * Fallback to servicing a SPC-2/3 compliant page 83 + * inquiry if the page 83 reply format does not + * conform to pre-SPC3 expectations. + */ + if (retval == 2) { + if (do_scsi_page83_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) { + retval = 1; + goto completed; + } else { + retval = 0; + goto completed; + } + } + else { + retval = 1; + goto completed; + } + } else { + retval = 0; + goto completed; + } + } else if (page_code != 0x00) { + log_debug("%s: unsupported page code 0x%d", dev_scsi->kernel, page_code); + retval = 1; + goto completed; + } + + /* + * Get page 0, the page of the pages. By default, try from best to + * worst of supported pages: 0x83 then 0x80. + */ + if (do_scsi_page0_inquiry(dev_scsi, fd, page0, SCSI_INQ_BUFF_LEN)) { + /* + * Don't try anything else. Black list if a specific page + * should be used for this vendor+model, or maybe have an + * optional fall-back to page 0x80 or page 0x83. + */ + retval = 1; + goto completed; + } + + for (ind = 4; ind <= page0[3] + 3; ind++) + if (page0[ind] == PAGE_83) + if (!do_scsi_page83_inquiry(dev_scsi, fd, + dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) { + /* + * Success + */ + retval = 0; + goto completed; + } + + for (ind = 4; ind <= page0[3] + 3; ind++) + if (page0[ind] == PAGE_80) + if (!do_scsi_page80_inquiry(dev_scsi, fd, + dev_scsi->serial, dev_scsi->serial_short, len)) { + /* + * Success + */ + retval = 0; + goto completed; + } + retval = 1; + +completed: + close(fd); + return retval; +} diff --git a/src/udev/test-udev-builtin.c b/src/udev/test-udev-builtin.c new file mode 100644 index 0000000..2ce7f19 --- /dev/null +++ b/src/udev/test-udev-builtin.c @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "tests.h" +#include "udev-builtin.h" + +TEST(udev_builtin_cmd_to_ptr) { + /* Those could have been static asserts, but ({}) is not allowed there. */ +#if HAVE_BLKID + assert_se(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BLKID)); + assert_se(PTR_TO_UDEV_BUILTIN_CMD(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BLKID)) == UDEV_BUILTIN_BLKID); +#endif + assert_se(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BTRFS)); + assert_se(PTR_TO_UDEV_BUILTIN_CMD(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BTRFS)) == UDEV_BUILTIN_BTRFS); + assert_se(PTR_TO_UDEV_BUILTIN_CMD(UDEV_BUILTIN_CMD_TO_PTR(_UDEV_BUILTIN_INVALID)) == _UDEV_BUILTIN_INVALID); + + assert_se(PTR_TO_UDEV_BUILTIN_CMD(NULL) == _UDEV_BUILTIN_INVALID); + assert_se(PTR_TO_UDEV_BUILTIN_CMD((void*) 10000) == _UDEV_BUILTIN_INVALID); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/udev/test-udev-format.c b/src/udev/test-udev-format.c new file mode 100644 index 0000000..d8e3808 --- /dev/null +++ b/src/udev/test-udev-format.c @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "string-util.h" +#include "tests.h" +#include "udev-format.h" + +static void test_udev_resolve_subsys_kernel_one(const char *str, bool read_value, int retval, const char *expected) { + char result[PATH_MAX] = ""; + int r; + + r = udev_resolve_subsys_kernel(str, result, sizeof(result), read_value); + log_info("\"%s\" → expect: \"%s\", %d, actual: \"%s\", %d", str, strnull(expected), retval, result, r); + assert_se(r == retval); + if (r >= 0) + assert_se(streq(result, expected)); +} + +TEST(udev_resolve_subsys_kernel) { + test_udev_resolve_subsys_kernel_one("hoge", false, -EINVAL, NULL); + test_udev_resolve_subsys_kernel_one("[hoge", false, -EINVAL, NULL); + test_udev_resolve_subsys_kernel_one("[hoge/foo", false, -EINVAL, NULL); + test_udev_resolve_subsys_kernel_one("[hoge/]", false, -EINVAL, NULL); + + test_udev_resolve_subsys_kernel_one("[net/lo]", false, 0, "/sys/devices/virtual/net/lo"); + test_udev_resolve_subsys_kernel_one("[net/lo]/", false, 0, "/sys/devices/virtual/net/lo"); + test_udev_resolve_subsys_kernel_one("[net/lo]hoge", false, 0, "/sys/devices/virtual/net/lo/hoge"); + test_udev_resolve_subsys_kernel_one("[net/lo]/hoge", false, 0, "/sys/devices/virtual/net/lo/hoge"); + + test_udev_resolve_subsys_kernel_one("[net/lo]", true, -EINVAL, NULL); + test_udev_resolve_subsys_kernel_one("[net/lo]/", true, -EINVAL, NULL); + test_udev_resolve_subsys_kernel_one("[net/lo]hoge", true, 0, ""); + test_udev_resolve_subsys_kernel_one("[net/lo]/hoge", true, 0, ""); + test_udev_resolve_subsys_kernel_one("[net/lo]address", true, 0, "00:00:00:00:00:00"); + test_udev_resolve_subsys_kernel_one("[net/lo]/address", true, 0, "00:00:00:00:00:00"); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/udev/test-udev-manager.c b/src/udev/test-udev-manager.c new file mode 100644 index 0000000..d444b0b --- /dev/null +++ b/src/udev/test-udev-manager.c @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "tests.h" +#include "udev-manager.h" + +TEST(devpath_conflict) { + assert_se(!devpath_conflict(NULL, NULL)); + assert_se(!devpath_conflict(NULL, "/devices/pci0000:00/0000:00:1c.4")); + assert_se(!devpath_conflict("/devices/pci0000:00/0000:00:1c.4", NULL)); + assert_se(!devpath_conflict("/devices/pci0000:00/0000:00:1c.4", "/devices/pci0000:00/0000:00:00.0")); + assert_se(!devpath_conflict("/devices/virtual/net/veth99", "/devices/virtual/net/veth999")); + + assert_se(devpath_conflict("/devices/pci0000:00/0000:00:1c.4", "/devices/pci0000:00/0000:00:1c.4")); + assert_se(devpath_conflict("/devices/pci0000:00/0000:00:1c.4", "/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0")); + assert_se(devpath_conflict("/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0/nvme0n1", + "/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0/nvme0n1/nvme0n1p1")); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/udev/test-udev-node.c b/src/udev/test-udev-node.c new file mode 100644 index 0000000..b5eaa0d --- /dev/null +++ b/src/udev/test-udev-node.c @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "tests.h" +#include "udev-node.h" + +static void test_udev_node_escape_path_one(const char *path, const char *expected) { + char buf[NAME_MAX+1]; + size_t r; + + r = udev_node_escape_path(path, buf, sizeof buf); + log_debug("udev_node_escape_path(%s) -> %s (expected: %s)", path, buf, expected); + assert_se(r == strlen(expected)); + assert_se(streq(buf, expected)); +} + +TEST(udev_node_escape_path) { + char a[NAME_MAX+1], b[NAME_MAX+1]; + + test_udev_node_escape_path_one("/disk/by-id/nvme-eui.1922908022470001001b448b44ccb9d6", "\\x2fdisk\\x2fby-id\\x2fnvme-eui.1922908022470001001b448b44ccb9d6"); + test_udev_node_escape_path_one("/disk/by-id/nvme-eui.1922908022470001001b448b44ccb9d6-part1", "\\x2fdisk\\x2fby-id\\x2fnvme-eui.1922908022470001001b448b44ccb9d6-part1"); + test_udev_node_escape_path_one("/disk/by-id/nvme-eui.1922908022470001001b448b44ccb9d6-part2", "\\x2fdisk\\x2fby-id\\x2fnvme-eui.1922908022470001001b448b44ccb9d6-part2"); + test_udev_node_escape_path_one("/disk/by-id/nvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247", "\\x2fdisk\\x2fby-id\\x2fnvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247"); + test_udev_node_escape_path_one("/disk/by-id/nvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part1", "\\x2fdisk\\x2fby-id\\x2fnvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part1"); + test_udev_node_escape_path_one("/disk/by-id/nvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part2", "\\x2fdisk\\x2fby-id\\x2fnvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part2"); + test_udev_node_escape_path_one("/disk/by-id/usb-Generic-_SD_MMC_20120501030900000-0:0", "\\x2fdisk\\x2fby-id\\x2fusb-Generic-_SD_MMC_20120501030900000-0:0"); + + memset(a, 'a', sizeof(a) - 1); + memcpy(a, "/disk/by-id/", strlen("/disk/by-id/")); + char_array_0(a); + + memset(b, 'a', sizeof(b) - 1); + memcpy(b, "\\x2fdisk\\x2fby-id\\x2f", strlen("\\x2fdisk\\x2fby-id\\x2f")); + strcpy(b + sizeof(b) - 12, "N3YhcCqFeID"); + + test_udev_node_escape_path_one(a, b); + + strcpy(a + sizeof(a) - 12 - 9, "N3YhcCqFeID"); + strcpy(b + sizeof(b) - 12, "L1oK9iKWdmi"); + test_udev_node_escape_path_one(a, b); + + strcpy(a + sizeof(a) - 12 - 9, "a"); + strcpy(b + sizeof(b) - 12, "A7oaHBRuuZq"); + test_udev_node_escape_path_one(a, b); + + a[sizeof(a) - 12 - 9] = '\0'; + b[sizeof(a) - 12] = '\0'; + test_udev_node_escape_path_one(a, b); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/udev/test-udev-rule-runner.c b/src/udev/test-udev-rule-runner.c new file mode 100644 index 0000000..72296b3 --- /dev/null +++ b/src/udev/test-udev-rule-runner.c @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/*** + Copyright © 2003-2004 Greg Kroah-Hartman <greg@kroah.com> +***/ + +#include <errno.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/mount.h> +#include <sys/signalfd.h> +#include <unistd.h> + +#include "device-private.h" +#include "fs-util.h" +#include "log.h" +#include "main-func.h" +#include "mkdir-label.h" +#include "mount-util.h" +#include "namespace-util.h" +#include "parse-util.h" +#include "selinux-util.h" +#include "signal-util.h" +#include "string-util.h" +#include "tests.h" +#include "udev-event.h" +#include "udev-spawn.h" +#include "version.h" + +static int device_new_from_synthetic_event(sd_device **ret, const char *syspath, const char *action) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + sd_device_action_t a; + int r; + + assert(ret); + assert(syspath); + assert(action); + + a = device_action_from_string(action); + if (a < 0) + return a; + + r = sd_device_new_from_syspath(&dev, syspath); + if (r < 0) + return r; + + r = device_read_uevent_file(dev); + if (r < 0) + return r; + + r = device_set_action(dev, a); + if (r < 0) + return r; + + *ret = TAKE_PTR(dev); + return 0; +} + +static int fake_filesystems(void) { + static const struct fakefs { + const char *src; + const char *target; + const char *error; + bool ignore_mount_error; + } fakefss[] = { + { "tmpfs/sys", "/sys", "Failed to mount test /sys", false }, + { "tmpfs/dev", "/dev", "Failed to mount test /dev", false }, + { "run", "/run", "Failed to mount test /run", false }, + { "run", "/etc/udev/rules.d", "Failed to mount empty /etc/udev/rules.d", true }, + { "run", UDEVLIBEXECDIR "/rules.d", "Failed to mount empty " UDEVLIBEXECDIR "/rules.d", true }, + }; + int r; + + r = detach_mount_namespace(); + if (r < 0) + return log_error_errno(r, "Failed to detach mount namespace: %m"); + + for (size_t i = 0; i < ELEMENTSOF(fakefss); i++) { + r = mount_nofollow_verbose(fakefss[i].ignore_mount_error ? LOG_NOTICE : LOG_ERR, + fakefss[i].src, fakefss[i].target, NULL, MS_BIND, NULL); + if (r < 0 && !fakefss[i].ignore_mount_error) + return r; + } + + return 0; +} + +static int run(int argc, char *argv[]) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_(udev_event_freep) UdevEvent *event = NULL; + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + const char *devpath, *devname, *action; + int r; + + test_setup_logging(LOG_INFO); + + if (!IN_SET(argc, 2, 3, 4)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This program needs between one and three arguments, %d given", argc - 1); + + r = fake_filesystems(); + if (r < 0) + return r; + + /* Let's make sure the test runs with selinux assumed disabled. */ +#if HAVE_SELINUX + fini_selinuxmnt(); +#endif + mac_selinux_retest(); + + if (argc == 2) { + if (!streq(argv[1], "check")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown argument: %s", argv[1]); + + return 0; + } + + log_debug("version %s", GIT_VERSION); + + r = mac_init(); + if (r < 0) + return r; + + action = argv[1]; + devpath = argv[2]; + + if (argv[3]) { + unsigned us; + + r = safe_atou(argv[3], &us); + if (r < 0) + return log_error_errno(r, "Invalid delay '%s': %m", argv[3]); + usleep_safe(us); + } + + assert_se(udev_rules_load(&rules, RESOLVE_NAME_EARLY) == 0); + + const char *syspath = strjoina("/sys", devpath); + r = device_new_from_synthetic_event(&dev, syspath, action); + if (r < 0) + return log_debug_errno(r, "Failed to open device '%s'", devpath); + + assert_se(event = udev_event_new(dev, 0, NULL, log_get_max_level())); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGHUP, SIGCHLD, -1) >= 0); + + /* do what devtmpfs usually provides us */ + if (sd_device_get_devname(dev, &devname) >= 0) { + const char *subsystem; + mode_t mode = 0600; + + if (sd_device_get_subsystem(dev, &subsystem) >= 0 && streq(subsystem, "block")) + mode |= S_IFBLK; + else + mode |= S_IFCHR; + + if (!streq(action, "remove")) { + dev_t devnum = makedev(0, 0); + + (void) mkdir_parents_label(devname, 0755); + (void) sd_device_get_devnum(dev, &devnum); + if (mknod(devname, mode, devnum) < 0) + return log_error_errno(errno, "mknod() failed for '%s': %m", devname); + } else { + if (unlink(devname) < 0) + return log_error_errno(errno, "unlink('%s') failed: %m", devname); + (void) rmdir_parents(devname, "/dev"); + } + } + + udev_event_execute_rules(event, -1, 3 * USEC_PER_SEC, SIGKILL, NULL, rules); + udev_event_execute_run(event, 3 * USEC_PER_SEC, SIGKILL); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/test-udev-rules.c b/src/udev/test-udev-rules.c new file mode 100644 index 0000000..b62b08b --- /dev/null +++ b/src/udev/test-udev-rules.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "string-util.h" +#include "tests.h" +#include "udev-rules.h" + +static void test_udev_rule_parse_value_one(const char *in, const char *expected_value, int expected_retval) { + _cleanup_free_ char *str = NULL; + char *value = UINT_TO_PTR(0x12345678U); + char *endpos = UINT_TO_PTR(0x87654321U); + + log_info("/* %s (%s, %s, %d) */", __func__, in, strnull(expected_value), expected_retval); + + assert_se(str = strdup(in)); + assert_se(udev_rule_parse_value(str, &value, &endpos) == expected_retval); + if (expected_retval < 0) { + /* not modified on failure */ + assert_se(value == UINT_TO_PTR(0x12345678U)); + assert_se(endpos == UINT_TO_PTR(0x87654321U)); + } else { + assert_se(streq_ptr(value, expected_value)); + assert_se(endpos == str + strlen(in)); + /* + * The return value must be terminated by two subsequent NULs + * so it could be safely interpreted as nulstr. + */ + assert_se(value[strlen(value) + 1] == '\0'); + } +} + +TEST(udev_rule_parse_value) { + /* input: "valid operand" + * parsed: valid operand + * use the following command to help generate textual C strings: + * python3 -c 'import json; print(json.dumps(input()))' */ + test_udev_rule_parse_value_one("\"valid operand\"", "valid operand", 0); + /* input: "va'l\'id\"op\"erand" + * parsed: va'l\'id"op"erand */ + test_udev_rule_parse_value_one("\"va'l\\'id\\\"op\\\"erand\"", "va'l\\'id\"op\"erand", 0); + test_udev_rule_parse_value_one("no quotes", NULL, -EINVAL); + test_udev_rule_parse_value_one("\"\\\\a\\b\\x\\y\"", "\\\\a\\b\\x\\y", 0); + test_udev_rule_parse_value_one("\"reject\0nul\"", NULL, -EINVAL); + /* input: e"" */ + test_udev_rule_parse_value_one("e\"\"", "", 0); + /* input: e"1234" */ + test_udev_rule_parse_value_one("e\"1234\"", "1234", 0); + /* input: e"\"" */ + test_udev_rule_parse_value_one("e\"\\\"\"", "\"", 0); + /* input: e"\ */ + test_udev_rule_parse_value_one("e\"\\", NULL, -EINVAL); + /* input: e"\" */ + test_udev_rule_parse_value_one("e\"\\\"", NULL, -EINVAL); + /* input: e"\\" */ + test_udev_rule_parse_value_one("e\"\\\\\"", "\\", 0); + /* input: e"\\\" */ + test_udev_rule_parse_value_one("e\"\\\\\\\"", NULL, -EINVAL); + /* input: e"\\\"" */ + test_udev_rule_parse_value_one("e\"\\\\\\\"\"", "\\\"", 0); + /* input: e"\\\\" */ + test_udev_rule_parse_value_one("e\"\\\\\\\\\"", "\\\\", 0); + /* input: e"operand with newline\n" */ + test_udev_rule_parse_value_one("e\"operand with newline\\n\"", "operand with newline\n", 0); + /* input: e"single\rcharacter\t\aescape\bsequence" */ + test_udev_rule_parse_value_one( + "e\"single\\rcharacter\\t\\aescape\\bsequence\"", "single\rcharacter\t\aescape\bsequence", 0); + /* input: e"reject\invalid escape sequence" */ + test_udev_rule_parse_value_one("e\"reject\\invalid escape sequence", NULL, -EINVAL); + /* input: e"\ */ + test_udev_rule_parse_value_one("e\"\\", NULL, -EINVAL); + /* input: "s\u1d1c\u1d04\u029c \u1d1c\u0274\u026a\u1d04\u1d0f\u1d05\u1d07 \U0001d568\U0001d560\U0001d568" */ + test_udev_rule_parse_value_one( + "e\"s\\u1d1c\\u1d04\\u029c \\u1d1c\\u0274\\u026a\\u1d04\\u1d0f\\u1d05\\u1d07 \\U0001d568\\U0001d560\\U0001d568\"", + "s\xe1\xb4\x9c\xe1\xb4\x84\xca\x9c \xe1\xb4\x9c\xc9\xb4\xc9\xaa\xe1\xb4\x84\xe1\xb4\x8f\xe1\xb4\x85\xe1\xb4\x87 \xf0\x9d\x95\xa8\xf0\x9d\x95\xa0\xf0\x9d\x95\xa8", + 0); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/udev/test-udev-spawn.c b/src/udev/test-udev-spawn.c new file mode 100644 index 0000000..4f43fac --- /dev/null +++ b/src/udev/test-udev-spawn.c @@ -0,0 +1,113 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "path-util.h" +#include "signal-util.h" +#include "strv.h" +#include "tests.h" +#include "udev-event.h" +#include "udev-spawn.h" + +#define BUF_SIZE 1024 + +static void test_event_spawn_core(bool with_pidfd, const char *cmd, char *result_buf, size_t buf_size) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + _cleanup_(udev_event_freep) UdevEvent *event = NULL; + + assert_se(setenv("SYSTEMD_PIDFD", yes_no(with_pidfd), 1) >= 0); + + assert_se(sd_device_new_from_syspath(&dev, "/sys/class/net/lo") >= 0); + assert_se(event = udev_event_new(dev, 0, NULL, LOG_DEBUG)); + assert_se(udev_event_spawn(event, 5 * USEC_PER_SEC, SIGKILL, false, cmd, result_buf, buf_size, NULL) == 0); + + assert_se(unsetenv("SYSTEMD_PIDFD") >= 0); +} + +static void test_event_spawn_cat(bool with_pidfd, size_t buf_size) { + _cleanup_strv_free_ char **lines = NULL; + _cleanup_free_ char *cmd = NULL; + char result_buf[BUF_SIZE]; + + log_debug("/* %s(%s) */", __func__, yes_no(with_pidfd)); + + assert_se(find_executable("cat", &cmd) >= 0); + assert_se(strextend_with_separator(&cmd, " ", "/sys/class/net/lo/uevent")); + + test_event_spawn_core(with_pidfd, cmd, result_buf, + buf_size >= BUF_SIZE ? BUF_SIZE : buf_size); + + assert_se(lines = strv_split_newlines(result_buf)); + strv_print(lines); + + if (buf_size >= BUF_SIZE) { + assert_se(strv_contains(lines, "INTERFACE=lo")); + assert_se(strv_contains(lines, "IFINDEX=1")); + } +} + +static void test_event_spawn_self(const char *self, const char *arg, bool with_pidfd) { + _cleanup_strv_free_ char **lines = NULL; + _cleanup_free_ char *cmd = NULL; + char result_buf[BUF_SIZE]; + + log_debug("/* %s(%s, %s) */", __func__, arg, yes_no(with_pidfd)); + + assert_se(cmd = strjoin(self, " ", arg)); + + test_event_spawn_core(with_pidfd, cmd, result_buf, BUF_SIZE); + + assert_se(lines = strv_split_newlines(result_buf)); + strv_print(lines); + + assert_se(strv_contains(lines, "aaa")); + assert_se(strv_contains(lines, "bbb")); +} + +static void test1(void) { + fprintf(stdout, "aaa\nbbb"); + fprintf(stderr, "ccc\nddd"); +} + +static void test2(void) { + char buf[16384]; + + fprintf(stdout, "aaa\nbbb"); + + memset(buf, 'a', sizeof(buf) - 1); + char_array_0(buf); + fputs(buf, stderr); +} + +int main(int argc, char *argv[]) { + _cleanup_free_ char *self = NULL; + + if (argc > 1) { + if (streq(argv[1], "test1")) + test1(); + else if (streq(argv[1], "test2")) + test2(); + else + assert_not_reached(); + + return 0; + } + + test_setup_logging(LOG_DEBUG); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0); + + test_event_spawn_cat(true, SIZE_MAX); + test_event_spawn_cat(false, SIZE_MAX); + test_event_spawn_cat(true, 5); + test_event_spawn_cat(false, 5); + + assert_se(path_make_absolute_cwd(argv[0], &self) >= 0); + path_simplify(self); + + test_event_spawn_self(self, "test1", true); + test_event_spawn_self(self, "test1", false); + + test_event_spawn_self(self, "test2", true); + test_event_spawn_self(self, "test2", false); + + return 0; +} diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c new file mode 100644 index 0000000..11419a3 --- /dev/null +++ b/src/udev/udev-builtin-blkid.c @@ -0,0 +1,474 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * probe disks for filesystems and partitions + * + * Copyright © 2011 Karel Zak <kzak@redhat.com> + */ + +#if HAVE_VALGRIND_MEMCHECK_H +#include <valgrind/memcheck.h> +#endif + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <linux/loop.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/stat.h> + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "blkid-util.h" +#include "device-util.h" +#include "devnum-util.h" +#include "efi-loader.h" +#include "errno-util.h" +#include "fd-util.h" +#include "gpt.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +static void print_property(sd_device *dev, bool test, const char *name, const char *value) { + char s[256]; + + s[0] = '\0'; + + if (streq(name, "TYPE")) { + udev_builtin_add_property(dev, test, "ID_FS_TYPE", value); + + } else if (streq(name, "USAGE")) { + udev_builtin_add_property(dev, test, "ID_FS_USAGE", value); + + } else if (streq(name, "VERSION")) { + udev_builtin_add_property(dev, test, "ID_FS_VERSION", value); + + } else if (streq(name, "UUID")) { + blkid_safe_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID", s); + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID_ENC", s); + + } else if (streq(name, "UUID_SUB")) { + blkid_safe_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID_SUB", s); + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID_SUB_ENC", s); + + } else if (streq(name, "LABEL")) { + blkid_safe_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_LABEL", s); + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_LABEL_ENC", s); + + } else if (STR_IN_SET(name, "FSSIZE", "FSLASTBLOCK", "FSBLOCKSIZE")) { + strscpyl(s, sizeof(s), "ID_FS_", name + 2, NULL); + udev_builtin_add_property(dev, test, s, value); + + } else if (streq(name, "PTTYPE")) { + udev_builtin_add_property(dev, test, "ID_PART_TABLE_TYPE", value); + + } else if (streq(name, "PTUUID")) { + udev_builtin_add_property(dev, test, "ID_PART_TABLE_UUID", value); + + } else if (streq(name, "PART_ENTRY_NAME")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_PART_ENTRY_NAME", s); + + } else if (streq(name, "PART_ENTRY_TYPE")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_PART_ENTRY_TYPE", s); + + } else if (startswith(name, "PART_ENTRY_")) { + strscpyl(s, sizeof(s), "ID_", name, NULL); + udev_builtin_add_property(dev, test, s, value); + + } else if (streq(name, "SYSTEM_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_SYSTEM_ID", s); + + } else if (streq(name, "PUBLISHER_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_PUBLISHER_ID", s); + + } else if (streq(name, "APPLICATION_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_APPLICATION_ID", s); + + } else if (streq(name, "BOOT_SYSTEM_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_BOOT_SYSTEM_ID", s); + + } else if (streq(name, "VOLUME_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_VOLUME_ID", s); + + } else if (streq(name, "LOGICAL_VOLUME_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_LOGICAL_VOLUME_ID", s); + + } else if (streq(name, "VOLUME_SET_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_VOLUME_SET_ID", s); + + } else if (streq(name, "DATA_PREPARER_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_DATA_PREPARER_ID", s); + } +} + +static int find_gpt_root(sd_device *dev, blkid_probe pr, bool test) { + +#if defined(SD_GPT_ROOT_NATIVE) && ENABLE_EFI + + _cleanup_free_ char *root_label = NULL; + bool found_esp_or_xbootldr = false; + sd_id128_t root_id = SD_ID128_NULL; + int r; + + assert(pr); + + /* Iterate through the partitions on this disk, and see if the UEFI ESP or XBOOTLDR partition we + * booted from is on it. If so, find the first root disk, and add a property indicating its partition + * UUID. */ + + errno = 0; + blkid_partlist pl = blkid_probe_get_partitions(pr); + if (!pl) + return errno_or_else(ENOMEM); + + int nvals = blkid_partlist_numof_partitions(pl); + for (int i = 0; i < nvals; i++) { + blkid_partition pp; + const char *label; + sd_id128_t type, id; + + pp = blkid_partlist_get_partition(pl, i); + if (!pp) + continue; + + r = blkid_partition_get_uuid_id128(pp, &id); + if (r < 0) { + log_debug_errno(r, "Failed to get partition UUID, ignoring: %m"); + continue; + } + + r = blkid_partition_get_type_id128(pp, &type); + if (r < 0) { + log_debug_errno(r, "Failed to get partition type UUID, ignoring: %m"); + continue; + } + + label = blkid_partition_get_name(pp); /* returns NULL if empty */ + + if (sd_id128_in_set(type, SD_GPT_ESP, SD_GPT_XBOOTLDR)) { + sd_id128_t esp_or_xbootldr; + + /* We found an ESP or XBOOTLDR, let's see if it matches the ESP/XBOOTLDR we booted from. */ + + r = efi_loader_get_device_part_uuid(&esp_or_xbootldr); + if (r < 0) + return r; + + if (sd_id128_equal(id, esp_or_xbootldr)) + found_esp_or_xbootldr = true; + + } else if (sd_id128_equal(type, SD_GPT_ROOT_NATIVE)) { + unsigned long long flags; + + flags = blkid_partition_get_flags(pp); + if (flags & SD_GPT_FLAG_NO_AUTO) + continue; + + /* We found a suitable root partition, let's remember the first one, or the one with + * the newest version, as determined by comparing the partition labels. */ + + if (sd_id128_is_null(root_id) || strverscmp_improved(label, root_label) > 0) { + root_id = id; + + r = free_and_strdup(&root_label, label); + if (r < 0) + return r; + } + } + } + + /* We found the ESP/XBOOTLDR on this disk, and also found a root partition, nice! Let's export its + * UUID */ + if (found_esp_or_xbootldr && !sd_id128_is_null(root_id)) + udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT_UUID", SD_ID128_TO_UUID_STRING(root_id)); +#endif + + return 0; +} + +static int probe_superblocks(blkid_probe pr) { + struct stat st; + int rc; + + /* TODO: Return negative errno. */ + + if (fstat(blkid_probe_get_fd(pr), &st)) + return -errno; + + blkid_probe_enable_partitions(pr, 1); + + if (!S_ISCHR(st.st_mode) && + blkid_probe_get_size(pr) <= 1024 * 1440 && + blkid_probe_is_wholedisk(pr)) { + /* + * check if the small disk is partitioned, if yes then + * don't probe for filesystems. + */ + blkid_probe_enable_superblocks(pr, 0); + + rc = blkid_do_fullprobe(pr); + if (rc < 0) + return rc; /* -1 = error, 1 = nothing, 0 = success */ + + if (blkid_probe_lookup_value(pr, "PTTYPE", NULL, NULL) == 0) + return 0; /* partition table detected */ + } + + blkid_probe_set_partitions_flags(pr, BLKID_PARTS_ENTRY_DETAILS); + blkid_probe_enable_superblocks(pr, 1); + + return blkid_do_safeprobe(pr); +} + +static int read_loopback_backing_inode( + sd_device *dev, + int fd, + dev_t *ret_devno, + ino_t *ret_inode, + char **ret_fname) { + + _cleanup_free_ char *fn = NULL; + struct loop_info64 info; + const char *name; + int r; + + assert(dev); + assert(fd >= 0); + assert(ret_devno); + assert(ret_inode); + assert(ret_fname); + + /* Retrieves various fields of the current loopback device backing file, so that we can ultimately + * use it to create stable symlinks to loopback block devices, based on what they are backed by. We + * pick up inode/device as well as file name field. Note that we pick up the "lo_file_name" field + * here, which is an arbitrary free-form string provided by userspace. We do not return the sysfs + * attribute loop/backing_file here, because that is directly accessible from udev rules anyway. And + * sometimes, depending on context, it's a good thing to return the string userspace can freely pick + * over the string automatically generated by the kernel. */ + + r = sd_device_get_sysname(dev, &name); + if (r < 0) + return r; + + if (!startswith(name, "loop")) + goto notloop; + + if (ioctl(fd, LOOP_GET_STATUS64, &info) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) + goto notloop; + + return -errno; + } + +#if HAVE_VALGRIND_MEMCHECK_H + VALGRIND_MAKE_MEM_DEFINED(&info, sizeof(info)); +#endif + + if (isempty((char*) info.lo_file_name) || + strnlen((char*) info.lo_file_name, sizeof(info.lo_file_name)-1) == sizeof(info.lo_file_name)-1) + /* Don't pick up file name if it is unset or possibly truncated. (Note: the kernel silently + * truncates the string passed from userspace by LOOP_SET_STATUS64 ioctl. See + * loop_set_status_from_info() in drivers/block/loop.c. Hence, we can't really know the file + * name is truncated if it uses sizeof(info.lo_file_name)-1 as length; it could also mean the + * string is just that long and wasn't truncated — but the fact is simply that we cannot know + * in that case if it was truncated or not. Thus, we assume the worst and suppress — at least + * for now. For shorter strings we know for sure it wasn't truncated, hence that's always + * safe.) */ + fn = NULL; + else { + fn = memdup_suffix0(info.lo_file_name, sizeof(info.lo_file_name)); + if (!fn) + return -ENOMEM; + } + + *ret_inode = info.lo_inode; + *ret_devno = info.lo_device; + *ret_fname = TAKE_PTR(fn); + return 1; + + +notloop: + *ret_devno = 0; + *ret_inode = 0; + *ret_fname = NULL; + return 0; +} + +static int builtin_blkid(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + const char *devnode, *root_partition = NULL, *data, *name; + _cleanup_(blkid_free_probep) blkid_probe pr = NULL; + _cleanup_free_ char *backing_fname = NULL; + bool noraid = false, is_gpt = false; + _cleanup_close_ int fd = -EBADF; + ino_t backing_inode = 0; + dev_t backing_devno = 0; + int64_t offset = 0; + int r; + + static const struct option options[] = { + { "offset", required_argument, NULL, 'o' }, + { "hint", required_argument, NULL, 'H' }, + { "noraid", no_argument, NULL, 'R' }, + {} + }; + + errno = 0; + pr = blkid_new_probe(); + if (!pr) + return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to create blkid prober: %m"); + + for (;;) { + int option; + + option = getopt_long(argc, argv, "o:H:R", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'H': +#if HAVE_BLKID_PROBE_SET_HINT + errno = 0; + r = blkid_probe_set_hint(pr, optarg, 0); + if (r < 0) + return log_device_error_errno(dev, errno_or_else(ENOMEM), "Failed to use '%s' probing hint: %m", optarg); + break; +#else + /* Use the hint <name>=<offset> as probing offset for old versions */ + optarg = strchr(optarg, '='); + if (!optarg) + /* no value means 0, do nothing for old versions */ + break; + ++optarg; + _fallthrough_; +#endif + case 'o': + r = safe_atoi64(optarg, &offset); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to parse '%s' as an integer: %m", optarg); + if (offset < 0) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid offset %"PRIi64": %m", offset); + break; + case 'R': + noraid = true; + break; + } + } + + blkid_probe_set_superblocks_flags(pr, + BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID | + BLKID_SUBLKS_TYPE | BLKID_SUBLKS_SECTYPE | +#ifdef BLKID_SUBLKS_FSINFO + BLKID_SUBLKS_FSINFO | +#endif + BLKID_SUBLKS_USAGE | BLKID_SUBLKS_VERSION); + + if (noraid) + blkid_probe_filter_superblocks_usage(pr, BLKID_FLTR_NOTIN, BLKID_USAGE_RAID); + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device name: %m"); + + fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) { + bool ignore = ERRNO_IS_DEVICE_ABSENT(fd); + log_device_debug_errno(dev, fd, "Failed to open block device %s%s: %m", + devnode, ignore ? ", ignoring" : ""); + return ignore ? 0 : fd; + } + + errno = 0; + r = blkid_probe_set_device(pr, fd, offset, 0); + if (r < 0) + return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to set device to blkid prober: %m"); + + log_device_debug(dev, "Probe %s with %sraid and offset=%"PRIi64, devnode, noraid ? "no" : "", offset); + + r = probe_superblocks(pr); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to probe superblocks: %m"); + + /* If the device is a partition then its parent passed the root partition UUID to the device */ + (void) sd_device_get_property_value(dev, "ID_PART_GPT_AUTO_ROOT_UUID", &root_partition); + + errno = 0; + int nvals = blkid_probe_numof_values(pr); + if (nvals < 0) + return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to get number of probed values: %m"); + + for (int i = 0; i < nvals; i++) { + if (blkid_probe_get_value(pr, i, &name, &data, NULL) < 0) + continue; + + print_property(dev, test, name, data); + + /* Is this a disk with GPT partition table? */ + if (streq(name, "PTTYPE") && streq(data, "gpt")) + is_gpt = true; + + /* Is this a partition that matches the root partition + * property inherited from the parent? */ + if (root_partition && streq(name, "PART_ENTRY_UUID") && streq(data, root_partition)) + udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT", "1"); + } + + if (is_gpt) + find_gpt_root(dev, pr, test); + + r = read_loopback_backing_inode( + dev, + fd, + &backing_devno, + &backing_inode, + &backing_fname); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to read loopback backing inode, ignoring: %m"); + else if (r > 0) { + udev_builtin_add_propertyf(dev, test, "ID_LOOP_BACKING_DEVICE", DEVNUM_FORMAT_STR, DEVNUM_FORMAT_VAL(backing_devno)); + udev_builtin_add_propertyf(dev, test, "ID_LOOP_BACKING_INODE", "%" PRIu64, (uint64_t) backing_inode); + + if (backing_fname) { + /* In the worst case blkid_encode_string() will blow up to 4x the string + * length. Hence size the buffer to 4x of the longest string + * read_loopback_backing_inode() might return */ + char encoded[sizeof_field(struct loop_info64, lo_file_name) * 4 + 1]; + + assert(strlen(backing_fname) < ELEMENTSOF(encoded) / 4); + blkid_encode_string(backing_fname, encoded, ELEMENTSOF(encoded)); + + udev_builtin_add_property(dev, test, "ID_LOOP_BACKING_FILENAME", backing_fname); + udev_builtin_add_property(dev, test, "ID_LOOP_BACKING_FILENAME_ENC", encoded); + } + } + + return 0; +} + +const UdevBuiltin udev_builtin_blkid = { + .name = "blkid", + .cmd = builtin_blkid, + .help = "Filesystem and partition probing", + .run_once = true, +}; diff --git a/src/udev/udev-builtin-btrfs.c b/src/udev/udev-builtin-btrfs.c new file mode 100644 index 0000000..9b12aeb --- /dev/null +++ b/src/udev/udev-builtin-btrfs.c @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <linux/btrfs.h> +#include <stdlib.h> +#include <sys/ioctl.h> + +#include "device-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +static int builtin_btrfs(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + struct btrfs_ioctl_vol_args args = {}; + _cleanup_close_ int fd = -EBADF; + int r; + + if (argc != 3 || !streq(argv[1], "ready")) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid arguments"); + + fd = open("/dev/btrfs-control", O_RDWR|O_CLOEXEC|O_NOCTTY); + if (fd < 0) { + if (ERRNO_IS_DEVICE_ABSENT(errno)) { + /* Driver not installed? Then we aren't ready. This is useful in initrds that lack + * btrfs.ko. After the host transition (where btrfs.ko will hopefully become + * available) the device can be retriggered and will then be considered ready. */ + udev_builtin_add_property(dev, test, "ID_BTRFS_READY", "0"); + return 0; + } + + return log_device_debug_errno(dev, errno, "Failed to open /dev/btrfs-control: %m"); + } + + strscpy(args.name, sizeof(args.name), argv[2]); + r = ioctl(fd, BTRFS_IOC_DEVICES_READY, &args); + if (r < 0) + return log_device_debug_errno(dev, errno, "Failed to call BTRFS_IOC_DEVICES_READY: %m"); + + udev_builtin_add_property(dev, test, "ID_BTRFS_READY", one_zero(r == 0)); + return 0; +} + +const UdevBuiltin udev_builtin_btrfs = { + .name = "btrfs", + .cmd = builtin_btrfs, + .help = "btrfs volume management", +}; diff --git a/src/udev/udev-builtin-hwdb.c b/src/udev/udev-builtin-hwdb.c new file mode 100644 index 0000000..19e07e7 --- /dev/null +++ b/src/udev/udev-builtin-hwdb.c @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fnmatch.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> + +#include "sd-hwdb.h" + +#include "alloc-util.h" +#include "device-util.h" +#include "hwdb-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "udev-builtin.h" + +static sd_hwdb *hwdb; + +int udev_builtin_hwdb_lookup(sd_device *dev, + const char *prefix, const char *modalias, + const char *filter, bool test) { + _cleanup_free_ char *lookup = NULL; + const char *key, *value; + int n = 0, r; + + if (!hwdb) + return -ENOENT; + + if (prefix) { + lookup = strjoin(prefix, modalias); + if (!lookup) + return -ENOMEM; + modalias = lookup; + } + + SD_HWDB_FOREACH_PROPERTY(hwdb, modalias, key, value) { + if (filter && fnmatch(filter, key, FNM_NOESCAPE) != 0) + continue; + + r = udev_builtin_add_property(dev, test, key, value); + if (r < 0) + return r; + n++; + } + return n; +} + +static const char *modalias_usb(sd_device *dev, char *s, size_t size) { + const char *v, *p, *n = NULL; + uint16_t vn, pn; + + if (sd_device_get_sysattr_value(dev, "idVendor", &v) < 0) + return NULL; + if (sd_device_get_sysattr_value(dev, "idProduct", &p) < 0) + return NULL; + if (safe_atoux16(v, &vn) < 0) + return NULL; + if (safe_atoux16(p, &pn) < 0) + return NULL; + (void) sd_device_get_sysattr_value(dev, "product", &n); + + (void) snprintf(s, size, "usb:v%04Xp%04X:%s", vn, pn, strempty(n)); + return s; +} + +static int udev_builtin_hwdb_search(sd_device *dev, sd_device *srcdev, + const char *subsystem, const char *prefix, + const char *filter, bool test) { + char s[LINE_MAX]; + bool last = false; + int r = 0; + + assert(dev); + + if (!srcdev) + srcdev = dev; + + for (sd_device *d = srcdev; d; ) { + const char *dsubsys, *devtype, *modalias = NULL; + + if (sd_device_get_subsystem(d, &dsubsys) < 0) + goto next; + + /* look only at devices of a specific subsystem */ + if (subsystem && !streq(dsubsys, subsystem)) + goto next; + + (void) sd_device_get_property_value(d, "MODALIAS", &modalias); + + if (streq(dsubsys, "usb") && + sd_device_get_devtype(d, &devtype) >= 0 && + streq(devtype, "usb_device")) { + /* if the usb_device does not have a modalias, compose one */ + if (!modalias) + modalias = modalias_usb(d, s, sizeof(s)); + + /* avoid looking at any parent device, they are usually just a USB hub */ + last = true; + } + + if (!modalias) + goto next; + + log_device_debug(dev, "hwdb modalias key: \"%s\"", modalias); + + r = udev_builtin_hwdb_lookup(dev, prefix, modalias, filter, test); + if (r > 0) + break; + + if (last) + break; +next: + if (sd_device_get_parent(d, &d) < 0) + break; + } + + return r; +} + +static int builtin_hwdb(UdevEvent *event, int argc, char *argv[], bool test) { + static const struct option options[] = { + { "filter", required_argument, NULL, 'f' }, + { "device", required_argument, NULL, 'd' }, + { "subsystem", required_argument, NULL, 's' }, + { "lookup-prefix", required_argument, NULL, 'p' }, + {} + }; + const char *filter = NULL; + const char *device = NULL; + const char *subsystem = NULL; + const char *prefix = NULL; + _cleanup_(sd_device_unrefp) sd_device *srcdev = NULL; + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + int r; + + if (!hwdb) + return -EINVAL; + + for (;;) { + int option; + + option = getopt_long(argc, argv, "f:d:s:p:", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'f': + filter = optarg; + break; + + case 'd': + device = optarg; + break; + + case 's': + subsystem = optarg; + break; + + case 'p': + prefix = optarg; + break; + } + } + + /* query a specific key given as argument */ + if (argv[optind]) { + r = udev_builtin_hwdb_lookup(dev, prefix, argv[optind], filter, test); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m"); + if (r == 0) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENODATA), "No entry found from hwdb."); + return r; + } + + /* read data from another device than the device we will store the data */ + if (device) { + r = sd_device_new_from_device_id(&srcdev, device); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to create sd_device object '%s': %m", device); + } + + r = udev_builtin_hwdb_search(dev, srcdev, subsystem, prefix, filter, test); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m"); + if (r == 0) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENODATA), "No entry found from hwdb."); + return r; +} + +/* called at udev startup and reload */ +static int builtin_hwdb_init(void) { + int r; + + if (hwdb) + return 0; + + r = sd_hwdb_new(&hwdb); + if (r < 0) + return r; + + return 0; +} + +/* called on udev shutdown and reload request */ +static void builtin_hwdb_exit(void) { + hwdb = sd_hwdb_unref(hwdb); +} + +/* called every couple of seconds during event activity; 'true' if config has changed */ +static bool builtin_hwdb_should_reload(void) { + if (hwdb_should_reload(hwdb)) { + log_debug("hwdb needs reloading."); + return true; + } + + return false; +} + +const UdevBuiltin udev_builtin_hwdb = { + .name = "hwdb", + .cmd = builtin_hwdb, + .init = builtin_hwdb_init, + .exit = builtin_hwdb_exit, + .should_reload = builtin_hwdb_should_reload, + .help = "Hardware database", +}; diff --git a/src/udev/udev-builtin-input_id.c b/src/udev/udev-builtin-input_id.c new file mode 100644 index 0000000..295e8d2 --- /dev/null +++ b/src/udev/udev-builtin-input_id.c @@ -0,0 +1,433 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * expose input properties via udev + * + * Portions Copyright © 2004 David Zeuthen, <david@fubar.dk> + * Copyright © 2014 Carlos Garnacho <carlosg@gnome.org> + */ + +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <unistd.h> +#include <linux/limits.h> + +#include "device-util.h" +#include "fd-util.h" +#include "missing_input.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "udev-builtin.h" + +/* we must use this kernel-compatible implementation */ +#define BITS_PER_LONG (sizeof(unsigned long) * 8) +#define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1) +#define OFF(x) ((x)%BITS_PER_LONG) +#define BIT(x) (1UL<<OFF(x)) +#define LONG(x) ((x)/BITS_PER_LONG) +#define test_bit(bit, array) ((array[LONG(bit)] >> OFF(bit)) & 1) + +struct range { + unsigned start; + unsigned end; +}; + +/* key code ranges above BTN_MISC (start is inclusive, stop is exclusive) */ +static const struct range high_key_blocks[] = { + { KEY_OK, BTN_DPAD_UP }, + { KEY_ALS_TOGGLE, BTN_TRIGGER_HAPPY } +}; + +static int abs_size_mm(const struct input_absinfo *absinfo) { + /* Resolution is defined to be in units/mm for ABS_X/Y */ + return (absinfo->maximum - absinfo->minimum) / absinfo->resolution; +} + +static void extract_info(sd_device *dev, bool test) { + char width[DECIMAL_STR_MAX(int)], height[DECIMAL_STR_MAX(int)]; + struct input_absinfo xabsinfo = {}, yabsinfo = {}; + _cleanup_close_ int fd = -EBADF; + + fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return; + + if (ioctl(fd, EVIOCGABS(ABS_X), &xabsinfo) < 0 || + ioctl(fd, EVIOCGABS(ABS_Y), &yabsinfo) < 0) + return; + + if (xabsinfo.resolution <= 0 || yabsinfo.resolution <= 0) + return; + + xsprintf(width, "%d", abs_size_mm(&xabsinfo)); + xsprintf(height, "%d", abs_size_mm(&yabsinfo)); + + udev_builtin_add_property(dev, test, "ID_INPUT_WIDTH_MM", width); + udev_builtin_add_property(dev, test, "ID_INPUT_HEIGHT_MM", height); +} + +/* + * Read a capability attribute and return bitmask. + * @param dev sd_device + * @param attr sysfs attribute name (e. g. "capabilities/key") + * @param bitmask: Output array which has a sizeof of bitmask_size + */ +static void get_cap_mask(sd_device *pdev, const char* attr, + unsigned long *bitmask, size_t bitmask_size, + bool test) { + const char *v; + char text[4096]; + unsigned i; + char* word; + unsigned long val; + int r; + + if (sd_device_get_sysattr_value(pdev, attr, &v) < 0) + v = ""; + + xsprintf(text, "%s", v); + log_device_debug(pdev, "%s raw kernel attribute: %s", attr, text); + + memzero(bitmask, bitmask_size); + i = 0; + while ((word = strrchr(text, ' '))) { + r = safe_atolu_full(word+1, 16, &val); + if (r < 0) + log_device_debug_errno(pdev, r, "Ignoring %s block which failed to parse: %m", attr); + else if (i < bitmask_size / sizeof(unsigned long)) + bitmask[i] = val; + else + log_device_debug(pdev, "Ignoring %s block %lX which is larger than maximum size", attr, val); + *word = '\0'; + i++; + } + r = safe_atolu_full(text, 16, &val); + if (r < 0) + log_device_debug_errno(pdev, r, "Ignoring %s block which failed to parse: %m", attr); + else if (i < bitmask_size / sizeof(unsigned long)) + bitmask[i] = val; + else + log_device_debug(pdev, "Ignoring %s block %lX which is larger than maximum size", attr, val); + + if (test && DEBUG_LOGGING) { + log_device_debug(pdev, "%s decoded bit map:", attr); + + val = bitmask_size / sizeof (unsigned long); + /* skip trailing zeros */ + while (bitmask[val-1] == 0 && val > 0) + --val; + + /* IN_SET() cannot be used in assert_cc(). */ + assert_cc(sizeof(unsigned long) == 4 || sizeof(unsigned long) == 8); + for (unsigned long j = 0; j < val; j++) + log_device_debug(pdev, + sizeof(unsigned long) == 4 ? " bit %4lu: %08lX\n" : " bit %4lu: %016lX\n", + j * BITS_PER_LONG, bitmask[j]); + } +} + +static struct input_id get_input_id(sd_device *dev) { + const char *v; + struct input_id id = {}; + + if (sd_device_get_sysattr_value(dev, "id/bustype", &v) >= 0) + (void) safe_atoux16(v, &id.bustype); + if (sd_device_get_sysattr_value(dev, "id/vendor", &v) >= 0) + (void) safe_atoux16(v, &id.vendor); + if (sd_device_get_sysattr_value(dev, "id/product", &v) >= 0) + (void) safe_atoux16(v, &id.product); + if (sd_device_get_sysattr_value(dev, "id/version", &v) >= 0) + (void) safe_atoux16(v, &id.version); + + return id; +} + +/* pointer devices */ +static bool test_pointers(sd_device *dev, + const struct input_id *id, + const unsigned long* bitmask_ev, + const unsigned long* bitmask_abs, + const unsigned long* bitmask_key, + const unsigned long* bitmask_rel, + const unsigned long* bitmask_props, + bool test) { + bool has_abs_coordinates = false; + bool has_rel_coordinates = false; + bool has_mt_coordinates = false; + size_t num_joystick_axes = 0; + size_t num_joystick_buttons = 0; + bool has_pad_buttons = false; + bool is_direct = false; + bool has_touch = false; + bool has_3d_coordinates = false; + bool has_keys = false; + bool has_stylus = false; + bool has_pen = false; + bool finger_but_no_pen = false; + bool has_mouse_button = false; + bool is_mouse = false; + bool is_abs_mouse = false; + bool is_touchpad = false; + bool is_touchscreen = false; + bool is_tablet = false; + bool is_tablet_pad = false; + bool is_joystick = false; + bool is_accelerometer = false; + bool is_pointing_stick = false; + bool has_wheel = false; + + has_keys = test_bit(EV_KEY, bitmask_ev); + has_abs_coordinates = test_bit(ABS_X, bitmask_abs) && test_bit(ABS_Y, bitmask_abs); + has_3d_coordinates = has_abs_coordinates && test_bit(ABS_Z, bitmask_abs); + is_accelerometer = test_bit(INPUT_PROP_ACCELEROMETER, bitmask_props); + + if (!has_keys && has_3d_coordinates) + is_accelerometer = true; + + if (is_accelerometer) { + udev_builtin_add_property(dev, test, "ID_INPUT_ACCELEROMETER", "1"); + return true; + } + + is_pointing_stick = test_bit(INPUT_PROP_POINTING_STICK, bitmask_props); + has_stylus = test_bit(BTN_STYLUS, bitmask_key); + has_pen = test_bit(BTN_TOOL_PEN, bitmask_key); + finger_but_no_pen = test_bit(BTN_TOOL_FINGER, bitmask_key) && !test_bit(BTN_TOOL_PEN, bitmask_key); + for (int button = BTN_MOUSE; button < BTN_JOYSTICK && !has_mouse_button; button++) + has_mouse_button = test_bit(button, bitmask_key); + has_rel_coordinates = test_bit(EV_REL, bitmask_ev) && test_bit(REL_X, bitmask_rel) && test_bit(REL_Y, bitmask_rel); + has_mt_coordinates = test_bit(ABS_MT_POSITION_X, bitmask_abs) && test_bit(ABS_MT_POSITION_Y, bitmask_abs); + + /* unset has_mt_coordinates if devices claims to have all abs axis */ + if (has_mt_coordinates && test_bit(ABS_MT_SLOT, bitmask_abs) && test_bit(ABS_MT_SLOT - 1, bitmask_abs)) + has_mt_coordinates = false; + is_direct = test_bit(INPUT_PROP_DIRECT, bitmask_props); + has_touch = test_bit(BTN_TOUCH, bitmask_key); + has_pad_buttons = test_bit(BTN_0, bitmask_key) && test_bit(BTN_1, bitmask_key) && !has_pen; + has_wheel = test_bit(EV_REL, bitmask_ev) && (test_bit(REL_WHEEL, bitmask_rel) || test_bit(REL_HWHEEL, bitmask_rel)); + + /* joysticks don't necessarily have buttons; e. g. + * rudders/pedals are joystick-like, but buttonless; they have + * other fancy axes. Others have buttons only but no axes. + * + * The BTN_JOYSTICK range starts after the mouse range, so a mouse + * with more than 16 buttons runs into the joystick range (e.g. Mad + * Catz Mad Catz M.M.O.TE). Skip those. + */ + if (!test_bit(BTN_JOYSTICK - 1, bitmask_key)) { + for (int button = BTN_JOYSTICK; button < BTN_DIGI; button++) + if (test_bit(button, bitmask_key)) + num_joystick_buttons++; + for (int button = BTN_TRIGGER_HAPPY1; button <= BTN_TRIGGER_HAPPY40; button++) + if (test_bit(button, bitmask_key)) + num_joystick_buttons++; + for (int button = BTN_DPAD_UP; button <= BTN_DPAD_RIGHT; button++) + if (test_bit(button, bitmask_key)) + num_joystick_buttons++; + } + for (int axis = ABS_RX; axis < ABS_PRESSURE; axis++) + if (test_bit(axis, bitmask_abs)) + num_joystick_axes++; + + if (has_abs_coordinates) { + if (has_stylus || has_pen) + is_tablet = true; + else if (finger_but_no_pen && !is_direct) + is_touchpad = true; + else if (has_mouse_button) + /* This path is taken by VMware's USB mouse, which has + * absolute axes, but no touch/pressure button. */ + is_abs_mouse = true; + else if (has_touch || is_direct) + is_touchscreen = true; + else if (num_joystick_buttons > 0 || num_joystick_axes > 0) + is_joystick = true; + } else if (num_joystick_buttons > 0 || num_joystick_axes > 0) + is_joystick = true; + + if (has_mt_coordinates) { + if (has_stylus || has_pen) + is_tablet = true; + else if (finger_but_no_pen && !is_direct) + is_touchpad = true; + else if (has_touch || is_direct) + is_touchscreen = true; + } + + if (is_tablet && has_pad_buttons) + is_tablet_pad = true; + + if (has_pad_buttons && has_wheel && !has_rel_coordinates) { + is_tablet = true; + is_tablet_pad = true; + } + + if (!is_tablet && !is_touchpad && !is_joystick && + has_mouse_button && + (has_rel_coordinates || + !has_abs_coordinates)) /* mouse buttons and no axis */ + is_mouse = true; + + /* There is no such thing as an i2c mouse */ + if (is_mouse && id->bustype == BUS_I2C) + is_pointing_stick = true; + + /* Joystick un-detection. Some keyboards have random joystick buttons + * set. Avoid those being labeled as ID_INPUT_JOYSTICK with some heuristics. + * The well-known keys represent a (randomly picked) set of key groups. + * A joystick may have one of those but probably not several. And a joystick with less than 2 buttons + * or axes is not a joystick either. + * libinput uses similar heuristics, any changes here should be added to libinput too. + */ + if (is_joystick) { + static const unsigned int well_known_keyboard_keys[] = { + KEY_LEFTCTRL, KEY_CAPSLOCK, KEY_NUMLOCK, KEY_INSERT, + KEY_MUTE, KEY_CALC, KEY_FILE, KEY_MAIL, KEY_PLAYPAUSE, + KEY_BRIGHTNESSDOWN, + }; + size_t num_well_known_keys = 0; + + if (has_keys) + for (size_t i = 0; i < ELEMENTSOF(well_known_keyboard_keys); i++) + if (test_bit(well_known_keyboard_keys[i], bitmask_key)) + num_well_known_keys++; + + if (num_well_known_keys >= 4 || num_joystick_buttons + num_joystick_axes < 2) { + log_device_debug(dev, "Input device has %zu joystick buttons and %zu axes but also %zu keyboard key sets, " + "assuming this is a keyboard, not a joystick.", + num_joystick_buttons, num_joystick_axes, num_well_known_keys); + is_joystick = false; + } + + if (has_wheel && has_pad_buttons) { + log_device_debug(dev, "Input device has %zu joystick buttons as well as tablet pad buttons, " + "assuming this is a tablet pad, not a joystick.", num_joystick_buttons); + + is_joystick = false; + } + } + + if (is_pointing_stick) + udev_builtin_add_property(dev, test, "ID_INPUT_POINTINGSTICK", "1"); + if (is_mouse || is_abs_mouse) + udev_builtin_add_property(dev, test, "ID_INPUT_MOUSE", "1"); + if (is_touchpad) + udev_builtin_add_property(dev, test, "ID_INPUT_TOUCHPAD", "1"); + if (is_touchscreen) + udev_builtin_add_property(dev, test, "ID_INPUT_TOUCHSCREEN", "1"); + if (is_joystick) + udev_builtin_add_property(dev, test, "ID_INPUT_JOYSTICK", "1"); + if (is_tablet) + udev_builtin_add_property(dev, test, "ID_INPUT_TABLET", "1"); + if (is_tablet_pad) + udev_builtin_add_property(dev, test, "ID_INPUT_TABLET_PAD", "1"); + + return is_tablet || is_mouse || is_abs_mouse || is_touchpad || is_touchscreen || is_joystick || is_pointing_stick; +} + +/* key like devices */ +static bool test_key(sd_device *dev, + const unsigned long* bitmask_ev, + const unsigned long* bitmask_key, + bool test) { + + bool found = false; + + /* do we have any KEY_* capability? */ + if (!test_bit(EV_KEY, bitmask_ev)) { + log_device_debug(dev, "test_key: no EV_KEY capability"); + return false; + } + + /* only consider KEY_* here, not BTN_* */ + for (size_t i = 0; i < BTN_MISC/BITS_PER_LONG && !found; i++) { + if (bitmask_key[i]) + found = true; + + log_device_debug(dev, "test_key: checking bit block %zu for any keys; found=%s", + i * BITS_PER_LONG, yes_no(found)); + } + /* If there are no keys in the lower block, check the higher blocks */ + for (size_t block = 0; block < sizeof(high_key_blocks) / sizeof(struct range) && !found; block++) + for (unsigned i = high_key_blocks[block].start; i < high_key_blocks[block].end && !found; i++) + if (test_bit(i, bitmask_key)) { + log_device_debug(dev, "test_key: Found key %x in high block", i); + found = true; + } + + if (found) + udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1"); + + /* the first 32 bits are ESC, numbers, and Q to D; if we have all of + * those, consider it a full keyboard; do not test KEY_RESERVED, though */ + if (FLAGS_SET(bitmask_key[0], 0xFFFFFFFE)) { + udev_builtin_add_property(dev, test, "ID_INPUT_KEYBOARD", "1"); + return true; + } + + return found; +} + +static int builtin_input_id(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *pdev, *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + unsigned long bitmask_ev[NBITS(EV_MAX)]; + unsigned long bitmask_abs[NBITS(ABS_MAX)]; + unsigned long bitmask_key[NBITS(KEY_MAX)]; + unsigned long bitmask_rel[NBITS(REL_MAX)]; + unsigned long bitmask_props[NBITS(INPUT_PROP_MAX)]; + const char *sysname; + bool is_pointer; + bool is_key; + + /* walk up the parental chain until we find the real input device; the + * argument is very likely a subdevice of this, like eventN */ + for (pdev = dev; pdev; ) { + const char *s; + + if (sd_device_get_sysattr_value(pdev, "capabilities/ev", &s) >= 0) + break; + + if (sd_device_get_parent_with_subsystem_devtype(pdev, "input", NULL, &pdev) >= 0) + continue; + + pdev = NULL; + break; + } + + if (pdev) { + struct input_id id = get_input_id(pdev); + + /* Use this as a flag that input devices were detected, so that this + * program doesn't need to be called more than once per device */ + udev_builtin_add_property(dev, test, "ID_INPUT", "1"); + get_cap_mask(pdev, "capabilities/ev", bitmask_ev, sizeof(bitmask_ev), test); + get_cap_mask(pdev, "capabilities/abs", bitmask_abs, sizeof(bitmask_abs), test); + get_cap_mask(pdev, "capabilities/rel", bitmask_rel, sizeof(bitmask_rel), test); + get_cap_mask(pdev, "capabilities/key", bitmask_key, sizeof(bitmask_key), test); + get_cap_mask(pdev, "properties", bitmask_props, sizeof(bitmask_props), test); + is_pointer = test_pointers(dev, &id, bitmask_ev, bitmask_abs, + bitmask_key, bitmask_rel, + bitmask_props, test); + is_key = test_key(dev, bitmask_ev, bitmask_key, test); + /* Some evdev nodes have only a scrollwheel */ + if (!is_pointer && !is_key && test_bit(EV_REL, bitmask_ev) && + (test_bit(REL_WHEEL, bitmask_rel) || test_bit(REL_HWHEEL, bitmask_rel))) + udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1"); + if (test_bit(EV_SW, bitmask_ev)) + udev_builtin_add_property(dev, test, "ID_INPUT_SWITCH", "1"); + + } + + if (sd_device_get_sysname(dev, &sysname) >= 0 && + startswith(sysname, "event")) + extract_info(dev, test); + + return 0; +} + +const UdevBuiltin udev_builtin_input_id = { + .name = "input_id", + .cmd = builtin_input_id, + .help = "Input device properties", +}; diff --git a/src/udev/udev-builtin-keyboard.c b/src/udev/udev-builtin-keyboard.c new file mode 100644 index 0000000..3903bc4 --- /dev/null +++ b/src/udev/udev-builtin-keyboard.c @@ -0,0 +1,252 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <linux/input.h> + +#include "device-util.h" +#include "fd-util.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +static const struct key_name *keyboard_lookup_key(const char *str, GPERF_LEN_TYPE len); +#include "keyboard-keys-from-name.h" + +static int install_force_release(sd_device *dev, const unsigned *release, unsigned release_count) { + sd_device *atkbd; + const char *cur; + char codes[4096]; + char *s; + size_t l; + unsigned i; + int r; + + assert(dev); + assert(release); + + r = sd_device_get_parent_with_subsystem_devtype(dev, "serio", NULL, &atkbd); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get serio parent: %m"); + + r = sd_device_get_sysattr_value(atkbd, "force_release", &cur); + if (r < 0) + return log_device_error_errno(atkbd, r, "Failed to get force-release attribute: %m"); + + s = codes; + l = sizeof(codes); + + /* copy current content */ + l = strpcpy(&s, l, cur); + + /* append new codes */ + for (i = 0; i < release_count; i++) + l = strpcpyf(&s, l, ",%u", release[i]); + + log_device_debug(atkbd, "keyboard: updating force-release list with '%s'", codes); + r = sd_device_set_sysattr_value(atkbd, "force_release", codes); + if (r < 0) + return log_device_error_errno(atkbd, r, "Failed to set force-release attribute: %m"); + + return 0; +} + +static int map_keycode(sd_device *dev, int fd, int scancode, const char *keycode) { + struct { + unsigned scan; + unsigned key; + } map; + const struct key_name *k; + unsigned keycode_num; + int r; + + /* translate identifier to key code */ + k = keyboard_lookup_key(keycode, strlen(keycode)); + if (k) + keycode_num = k->id; + else { + /* check if it's a numeric code already */ + r = safe_atou(keycode, &keycode_num); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to parse key identifier '%s': %m", keycode); + } + + map.scan = scancode; + map.key = keycode_num; + + log_device_debug(dev, "keyboard: mapping scan code %u (0x%x) to key code %u (0x%x)", + map.scan, map.scan, map.key, map.key); + + if (ioctl(fd, EVIOCSKEYCODE, &map) < 0) + return log_device_error_errno(dev, errno, "Failed to call EVIOCSKEYCODE with scan code 0x%x, and key code %u: %m", map.scan, map.key); + + return 0; +} + +static const char* parse_token(const char *current, int32_t *val_out) { + char *next; + int32_t val; + + if (!current) + return NULL; + + val = strtol(current, &next, 0); + if (*next && *next != ':') + return NULL; + + if (next != current) + *val_out = val; + + if (*next) + next++; + + return next; +} + +static int override_abs(sd_device *dev, int fd, unsigned evcode, const char *value) { + struct input_absinfo absinfo; + const char *next; + + if (ioctl(fd, EVIOCGABS(evcode), &absinfo) < 0) + return log_device_error_errno(dev, errno, "Failed to call EVIOCGABS"); + + next = parse_token(value, &absinfo.minimum); + next = parse_token(next, &absinfo.maximum); + next = parse_token(next, &absinfo.resolution); + next = parse_token(next, &absinfo.fuzz); + next = parse_token(next, &absinfo.flat); + if (!next) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "Failed to parse EV_ABS override '%s'", value); + + log_device_debug(dev, "keyboard: %x overridden with %"PRIi32"/%"PRIi32"/%"PRIi32"/%"PRIi32"/%"PRIi32, + evcode, absinfo.minimum, absinfo.maximum, absinfo.resolution, absinfo.fuzz, absinfo.flat); + if (ioctl(fd, EVIOCSABS(evcode), &absinfo) < 0) + return log_device_error_errno(dev, errno, "Failed to call EVIOCSABS"); + + return 0; +} + +static int set_trackpoint_sensitivity(sd_device *dev, const char *value) { + sd_device *pdev; + char val_s[DECIMAL_STR_MAX(int)]; + int r, val_i; + + assert(dev); + assert(value); + + /* The sensitivity sysfs attr belongs to the serio parent device */ + r = sd_device_get_parent_with_subsystem_devtype(dev, "serio", NULL, &pdev); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get serio parent: %m"); + + r = safe_atoi(value, &val_i); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to parse POINTINGSTICK_SENSITIVITY '%s': %m", value); + else if (val_i < 0 || val_i > 255) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(ERANGE), "POINTINGSTICK_SENSITIVITY %d outside range [0..255]", val_i); + + xsprintf(val_s, "%d", val_i); + + r = sd_device_set_sysattr_value(pdev, "sensitivity", val_s); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to write 'sensitivity' attribute: %m"); + + return 0; +} + +static int builtin_keyboard(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + unsigned release[1024]; + unsigned release_count = 0; + _cleanup_close_ int fd = -EBADF; + const char *node; + int has_abs = -1, r; + + r = sd_device_get_devname(dev, &node); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get device name: %m"); + + FOREACH_DEVICE_PROPERTY(dev, key, value) + if (startswith(key, "KEYBOARD_KEY_")) { + const char *keycode = value; + unsigned scancode; + + /* KEYBOARD_KEY_<hex scan code>=<key identifier string> */ + r = safe_atou_full(key + 13, 16, &scancode); + if (r < 0) { + log_device_warning_errno(dev, r, "Failed to parse scan code from \"%s\", ignoring: %m", key); + continue; + } + + /* a leading '!' needs a force-release entry */ + if (keycode[0] == '!') { + keycode++; + + release[release_count] = scancode; + if (release_count < ELEMENTSOF(release)-1) + release_count++; + + if (keycode[0] == '\0') + continue; + } + + if (fd < 0) { + fd = sd_device_open(dev, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return log_device_error_errno(dev, fd, "Failed to open device '%s': %m", node); + } + + (void) map_keycode(dev, fd, scancode, keycode); + } else if (startswith(key, "EVDEV_ABS_")) { + unsigned evcode; + + /* EVDEV_ABS_<EV_ABS code>=<min>:<max>:<res>:<fuzz>:<flat> */ + r = safe_atou_full(key + 10, 16, &evcode); + if (r < 0) { + log_device_warning_errno(dev, r, "Failed to parse EV_ABS code from \"%s\", ignoring: %m", key); + continue; + } + + if (fd < 0) { + fd = sd_device_open(dev, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return log_device_error_errno(dev, fd, "Failed to open device '%s': %m", node); + } + + if (has_abs == -1) { + unsigned long bits; + int rc; + + rc = ioctl(fd, EVIOCGBIT(0, sizeof(bits)), &bits); + if (rc < 0) + return log_device_error_errno(dev, errno, "Failed to set EVIOCGBIT"); + + has_abs = !!(bits & (1 << EV_ABS)); + if (!has_abs) + log_device_warning(dev, "EVDEV_ABS override set but no EV_ABS present on device"); + } + + if (!has_abs) + continue; + + (void) override_abs(dev, fd, evcode, value); + } else if (streq(key, "POINTINGSTICK_SENSITIVITY")) + (void) set_trackpoint_sensitivity(dev, value); + + /* install list of force-release codes */ + if (release_count > 0) + (void) install_force_release(dev, release, release_count); + + return 0; +} + +const UdevBuiltin udev_builtin_keyboard = { + .name = "keyboard", + .cmd = builtin_keyboard, + .help = "Keyboard scancode mapping and touchpad/pointingstick characteristics", +}; diff --git a/src/udev/udev-builtin-kmod.c b/src/udev/udev-builtin-kmod.c new file mode 100644 index 0000000..3ab5c48 --- /dev/null +++ b/src/udev/udev-builtin-kmod.c @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * load kernel modules + * + * Copyright © 2011 ProFUSION embedded systems + */ + +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "device-util.h" +#include "module-util.h" +#include "string-util.h" +#include "strv.h" +#include "udev-builtin.h" + +static struct kmod_ctx *ctx = NULL; + +_printf_(6,0) static void udev_kmod_log(void *data, int priority, const char *file, int line, const char *fn, const char *format, va_list args) { + log_internalv(priority, 0, file, line, fn, format, args); +} + +static int builtin_kmod(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + int r; + + if (!ctx) + return 0; + + if (argc < 2 || !streq(argv[1], "load")) + return log_device_warning_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "%s: expected: load [module…]", argv[0]); + + char **modules = strv_skip(argv, 2); + if (strv_isempty(modules)) { + const char *modalias; + + r = sd_device_get_property_value(dev, "MODALIAS", &modalias); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to read property \"MODALIAS\"."); + + (void) module_load_and_warn(ctx, modalias, /* verbose = */ false); + } else + STRV_FOREACH(module, modules) + (void) module_load_and_warn(ctx, *module, /* verbose = */ false); + + return 0; +} + +/* called at udev startup and reload */ +static int builtin_kmod_init(void) { + if (ctx) + return 0; + + ctx = kmod_new(NULL, NULL); + if (!ctx) + return -ENOMEM; + + log_debug("Loading kernel module index."); + kmod_set_log_fn(ctx, udev_kmod_log, NULL); + kmod_load_resources(ctx); + return 0; +} + +/* called on udev shutdown and reload request */ +static void builtin_kmod_exit(void) { + log_debug("Unload kernel module index."); + ctx = kmod_unref(ctx); +} + +/* called every couple of seconds during event activity; 'true' if config has changed */ +static bool builtin_kmod_should_reload(void) { + if (!ctx) + return false; + + if (kmod_validate_resources(ctx) != KMOD_RESOURCES_OK) { + log_debug("Kernel module index needs reloading."); + return true; + } + + return false; +} + +const UdevBuiltin udev_builtin_kmod = { + .name = "kmod", + .cmd = builtin_kmod, + .init = builtin_kmod_init, + .exit = builtin_kmod_exit, + .should_reload = builtin_kmod_should_reload, + .help = "Kernel module loader", + .run_once = false, +}; diff --git a/src/udev/udev-builtin-net_driver.c b/src/udev/udev-builtin-net_driver.c new file mode 100644 index 0000000..f1642a4 --- /dev/null +++ b/src/udev/udev-builtin-net_driver.c @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "device-util.h" +#include "errno-util.h" +#include "ethtool-util.h" +#include "fd-util.h" +#include "log.h" +#include "string-util.h" +#include "udev-builtin.h" + +static int builtin_net_driver_set_driver(UdevEvent *event, int argc, char **argv, bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + _cleanup_close_ int ethtool_fd = -EBADF; + _cleanup_free_ char *driver = NULL; + const char *sysname; + int r; + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get sysname: %m"); + + r = ethtool_get_driver(ðtool_fd, sysname, &driver); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) { + log_device_debug_errno(dev, r, "Querying driver name via ethtool API is not supported by device '%s', ignoring: %m", sysname); + return 0; + } + if (r == -ENODEV) { + log_device_debug_errno(dev, r, "Device already vanished, ignoring."); + return 0; + } + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get driver for '%s': %m", sysname); + + return udev_builtin_add_property(event->dev, test, "ID_NET_DRIVER", driver); +} + +const UdevBuiltin udev_builtin_net_driver = { + .name = "net_driver", + .cmd = builtin_net_driver_set_driver, + .help = "Set driver for network device", + .run_once = true, +}; diff --git a/src/udev/udev-builtin-net_id.c b/src/udev/udev-builtin-net_id.c new file mode 100644 index 0000000..91b4008 --- /dev/null +++ b/src/udev/udev-builtin-net_id.c @@ -0,0 +1,1366 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * Predictable network interface device names based on: + * - firmware/bios-provided index numbers for on-board devices + * - firmware-provided pci-express hotplug slot index number + * - physical/geographical location of the hardware + * - the interface's MAC address + * + * https://systemd.io/PREDICTABLE_INTERFACE_NAMES + * + * When the code here is changed, man/systemd.net-naming-scheme.xml must be updated too. + */ + +#include <errno.h> +#include <fcntl.h> +#include <net/if.h> +#include <stdarg.h> +#include <unistd.h> +#include <linux/if.h> +#include <linux/if_arp.h> +#include <linux/netdevice.h> +#include <linux/pci_regs.h> + +#include "alloc-util.h" +#include "chase.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "ether-addr-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "glyph-util.h" +#include "netif-naming-scheme.h" +#include "parse-util.h" +#include "proc-cmdline.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +#define ONBOARD_14BIT_INDEX_MAX ((1U << 14) - 1) +#define ONBOARD_16BIT_INDEX_MAX ((1U << 16) - 1) + +/* skip intermediate virtio devices */ +static sd_device *device_skip_virtio(sd_device *dev) { + /* there can only ever be one virtio bus per parent device, so we can + * safely ignore any virtio buses. see + * http://lists.linuxfoundation.org/pipermail/virtualization/2015-August/030331.html */ + while (dev) { + const char *subsystem; + + if (sd_device_get_subsystem(dev, &subsystem) < 0) + break; + + if (!streq(subsystem, "virtio")) + break; + + if (sd_device_get_parent(dev, &dev) < 0) + return NULL; + } + + return dev; +} + +static int get_matching_parent( + sd_device *dev, + char * const *parent_subsystems, + bool skip_virtio, + sd_device **ret) { + + sd_device *parent; + int r; + + assert(dev); + + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return r; + + if (skip_virtio) { + /* skip virtio subsystem if present */ + parent = device_skip_virtio(parent); + if (!parent) + return -ENODEV; + } + + if (!strv_isempty(parent_subsystems)) { + const char *subsystem; + + /* check if our direct parent is in an expected subsystem. */ + r = sd_device_get_subsystem(parent, &subsystem); + if (r < 0) + return r; + + if (!strv_contains(parent_subsystems, subsystem)) + return -ENODEV; + } + + if (ret) + *ret = parent; + + return 0; +} + +static int get_first_syspath_component(sd_device *dev, const char *prefix, char **ret) { + _cleanup_free_ char *buf = NULL; + const char *syspath, *p, *q; + int r; + + assert(dev); + assert(prefix); + assert(ret); + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + p = path_startswith(syspath, prefix); + if (!p) + return -EINVAL; + + r = path_find_first_component(&p, /* accept_dot_dot = */ false, &q); + if (r < 0) + return r; + + buf = strndup(q, r); + if (!buf) + return -ENOMEM; + + *ret = TAKE_PTR(buf); + return r; /* return the length of the string */ +} + +static int get_virtfn_info(sd_device *pcidev, sd_device **ret_physfn_pcidev, char **ret_suffix) { + _cleanup_(sd_device_unrefp) sd_device *physfn_pcidev = NULL; + const char *syspath, *name; + int r; + + assert(pcidev); + assert(ret_physfn_pcidev); + assert(ret_suffix); + + r = sd_device_get_syspath(pcidev, &syspath); + if (r < 0) + return r; + + /* Get physical function's pci device. */ + r = sd_device_new_child(&physfn_pcidev, pcidev, "physfn"); + if (r < 0) + return r; + + /* Find the virtual function number by finding the right virtfn link. */ + FOREACH_DEVICE_CHILD_WITH_SUFFIX(physfn_pcidev, child, name) { + const char *n, *s; + + /* Only accepts e.g. virtfn0, virtfn1, and so on. */ + n = startswith(name, "virtfn"); + if (isempty(n) || !in_charset(n, DIGITS)) + continue; + + if (sd_device_get_syspath(child, &s) < 0) + continue; + + if (streq(s, syspath)) { + char *suffix; + + suffix = strjoin("v", n); + if (!suffix) + return -ENOMEM; + + *ret_physfn_pcidev = sd_device_ref(physfn_pcidev); + *ret_suffix = suffix; + return 0; + } + } + + return -ENOENT; +} + +static int get_dev_port(sd_device *dev, bool fallback_to_dev_id, unsigned *ret) { + unsigned v; + int r; + + assert(dev); + assert(ret); + + /* Get kernel provided port index for the case when multiple ports on a single PCI function. */ + + r = device_get_sysattr_unsigned(dev, "dev_port", &v); + if (r < 0) + return r; + if (r > 0) { + /* Found a positive index. Let's use it. */ + *ret = v; + return 1; /* positive */ + } + assert(v == 0); + + /* With older kernels IP-over-InfiniBand network interfaces sometimes erroneously provide the port + * number in the 'dev_id' sysfs attribute instead of 'dev_port', which thus stays initialized as 0. */ + + if (fallback_to_dev_id) { + unsigned iftype; + + r = device_get_sysattr_unsigned(dev, "type", &iftype); + if (r < 0) + return r; + + fallback_to_dev_id = (iftype == ARPHRD_INFINIBAND); + } + + if (fallback_to_dev_id) + return device_get_sysattr_unsigned(dev, "dev_id", ret); + + /* Otherwise, return the original index 0. */ + *ret = 0; + return 0; /* zero */ +} + +static int get_port_specifier(sd_device *dev, bool fallback_to_dev_id, char **ret) { + const char *phys_port_name; + unsigned dev_port; + char *buf; + int r; + + assert(dev); + assert(ret); + + /* First, try to use the kernel provided front panel port name for multiple port PCI device. */ + r = sd_device_get_sysattr_value(dev, "phys_port_name", &phys_port_name); + if (r >= 0 && !isempty(phys_port_name)) { + if (naming_scheme_has(NAMING_SR_IOV_R)) { + int vf_id = -1; + + /* Check if phys_port_name indicates virtual device representor. */ + (void) sscanf(phys_port_name, "pf%*uvf%d", &vf_id); + + if (vf_id >= 0) { + /* For VF representor append 'r<VF_NUM>'. */ + if (asprintf(&buf, "r%d", vf_id) < 0) + return log_oom_debug(); + + *ret = buf; + return 1; + } + } + + /* Otherwise, use phys_port_name as is. */ + buf = strjoin("n", phys_port_name); + if (!buf) + return log_oom_debug(); + + *ret = buf; + return 1; + } + + /* Then, try to use the kernel provided port index for the case when multiple ports on a single PCI + * function. */ + r = get_dev_port(dev, fallback_to_dev_id, &dev_port); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device port index: %m"); + if (r > 0) { + assert(dev_port > 0); + if (asprintf(&buf, "d%u", dev_port) < 0) + return log_oom_debug(); + + *ret = buf; + return 1; + } + + *ret = NULL; + return 0; +} + +static bool is_valid_onboard_index(unsigned idx) { + /* Some BIOSes report rubbish indexes that are excessively high (2^24-1 is an index VMware likes to + * report for example). Let's define a cut-off where we don't consider the index reliable anymore. We + * pick some arbitrary cut-off, which is somewhere beyond the realistic number of physical network + * interface a system might have. Ideally the kernel would already filter this crap for us, but it + * doesn't currently. The initial cut-off value (2^14-1) was too conservative for s390 PCI which + * allows for index values up 2^16-1 which is now enabled with the NAMING_16BIT_INDEX naming flag. */ + return idx <= (naming_scheme_has(NAMING_16BIT_INDEX) ? ONBOARD_16BIT_INDEX_MAX : ONBOARD_14BIT_INDEX_MAX); +} + +static int pci_get_onboard_index(sd_device *dev, unsigned *ret) { + unsigned idx; + int r; + + assert(dev); + assert(ret); + + /* ACPI _DSM — device specific method for naming a PCI or PCI Express device */ + r = device_get_sysattr_unsigned(dev, "acpi_index", &idx); + if (r < 0) + /* SMBIOS type 41 — Onboard Devices Extended Information */ + r = device_get_sysattr_unsigned(dev, "index", &idx); + if (r < 0) + return log_device_debug_errno(dev, r, "Could not obtain onboard index: %m"); + + if (idx == 0 && !naming_scheme_has(NAMING_ZERO_ACPI_INDEX)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "Naming scheme does not allow onboard index==0."); + if (!is_valid_onboard_index(idx)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENOENT), + "Not a valid onboard index: %u", idx); + + *ret = idx; + return 0; +} + +static int names_pci_onboard(sd_device *dev, sd_device *pci_dev, const char *prefix, const char *suffix, bool test) { + _cleanup_free_ char *port = NULL; + unsigned idx = 0; /* avoid false maybe-uninitialized warning */ + int r; + + assert(dev); + assert(pci_dev); + assert(prefix); + + /* retrieve on-board index number from firmware */ + r = pci_get_onboard_index(pci_dev, &idx); + if (r < 0) + return r; + + r = get_port_specifier(dev, /* fallback_to_dev_id = */ false, &port); + if (r < 0) + return r; + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%so%u%s%s", prefix, idx, strempty(port), strempty(suffix))) + udev_builtin_add_property(dev, test, "ID_NET_NAME_ONBOARD", str); + + log_device_debug(dev, "Onboard index identifier: index=%u port=%s %s %s", + idx, strna(port), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), empty_to_na(str)); + + return 0; +} + +static int names_pci_onboard_label(sd_device *dev, sd_device *pci_dev, const char *prefix, bool test) { + const char *label; + int r; + + assert(dev); + assert(prefix); + + /* retrieve on-board label from firmware */ + r = sd_device_get_sysattr_value(pci_dev, "label", &label); + if (r < 0) + return log_device_debug_errno(pci_dev, r, "Failed to get PCI onboard label: %m"); + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%s%s", + naming_scheme_has(NAMING_LABEL_NOPREFIX) ? "" : prefix, + label)) + udev_builtin_add_property(dev, test, "ID_NET_LABEL_ONBOARD", str); + + log_device_debug(dev, "Onboard label from PCI device: %s", label); + return 0; +} + +/* read the 256 bytes PCI configuration space to check the multi-function bit */ +static int is_pci_multifunction(sd_device *dev) { + _cleanup_free_ uint8_t *config = NULL; + const char *filename, *syspath; + size_t len; + int r; + + assert(dev); + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + filename = strjoina(syspath, "/config"); + r = read_virtual_file(filename, PCI_HEADER_TYPE + 1, (char **) &config, &len); + if (r < 0) + return r; + if (len < PCI_HEADER_TYPE + 1) + return -EINVAL; + +#ifndef PCI_HEADER_TYPE_MULTIFUNC +#define PCI_HEADER_TYPE_MULTIFUNC 0x80 +#endif + + /* bit 0-6 header type, bit 7 multi/single function device */ + return config[PCI_HEADER_TYPE] & PCI_HEADER_TYPE_MULTIFUNC; +} + +static bool is_pci_ari_enabled(sd_device *dev) { + assert(dev); + + return device_get_sysattr_bool(dev, "ari_enabled") > 0; +} + +static bool is_pci_bridge(sd_device *dev) { + const char *v, *p; + + assert(dev); + + if (sd_device_get_sysattr_value(dev, "modalias", &v) < 0) + return false; + + if (!startswith(v, "pci:")) + return false; + + p = strrchr(v, 's'); + if (!p) + return false; + if (p[1] != 'c') + return false; + + /* PCI device subclass 04 corresponds to PCI bridge */ + bool b = strneq(p + 2, "04", 2); + if (b) + log_device_debug(dev, "Device is a PCI bridge."); + return b; +} + +static int parse_hotplug_slot_from_function_id(sd_device *dev, int slots_dirfd, uint32_t *ret) { + uint64_t function_id; + char filename[NAME_MAX+1]; + const char *attr; + int r; + + /* The <sysname>/function_id attribute is unique to the s390 PCI driver. If present, we know that the + * slot's directory name for this device is /sys/bus/pci/slots/XXXXXXXX/ where XXXXXXXX is the fixed + * length 8 hexadecimal character string representation of function_id. Therefore we can short cut + * here and just check for the existence of the slot directory. As this directory has to exist, we're + * emitting a debug message for the unlikely case it's not found. Note that the domain part doesn't + * belong to the slot name here because there's a 1-to-1 relationship between PCI function and its + * hotplug slot. See https://docs.kernel.org/s390/pci.html for more details. */ + + assert(dev); + assert(slots_dirfd >= 0); + assert(ret); + + if (!naming_scheme_has(NAMING_SLOT_FUNCTION_ID)) { + *ret = 0; + return 0; + } + + if (sd_device_get_sysattr_value(dev, "function_id", &attr) < 0) { + *ret = 0; + return 0; + } + + r = safe_atou64(attr, &function_id); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to parse function_id, ignoring: %s", attr); + + if (function_id <= 0 || function_id > UINT32_MAX) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "Invalid function id (0x%"PRIx64"), ignoring.", + function_id); + + if (!snprintf_ok(filename, sizeof(filename), "%08"PRIx64, function_id)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENAMETOOLONG), + "PCI slot path is too long, ignoring."); + + if (faccessat(slots_dirfd, filename, F_OK, 0) < 0) + return log_device_debug_errno(dev, errno, "Cannot access %s under pci slots, ignoring: %m", filename); + + *ret = (uint32_t) function_id; + return 1; /* Found. We should ignore domain part. */ +} + +static int pci_get_hotplug_slot_from_address( + sd_device *dev, + sd_device *pci, + DIR *dir, + uint32_t *ret) { + + const char *sysname; + int r; + + assert(dev); + assert(pci); + assert(dir); + assert(ret); + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + rewinddir(dir); + FOREACH_DIRENT_ALL(de, dir, break) { + _cleanup_free_ char *path = NULL; + const char *address; + uint32_t slot; + + if (dot_or_dot_dot(de->d_name)) + continue; + + if (de->d_type != DT_DIR) + continue; + + r = safe_atou32(de->d_name, &slot); + if (r < 0 || slot <= 0) + continue; + + path = path_join("slots", de->d_name, "address"); + if (!path) + return log_oom_debug(); + + if (sd_device_get_sysattr_value(pci, path, &address) < 0) + continue; + + /* match slot address with device by stripping the function */ + if (!startswith(sysname, address)) + continue; + + *ret = slot; + return 1; /* found */ + } + + *ret = 0; + return 0; /* not found */ +} + +static int pci_get_hotplug_slot(sd_device *dev, uint32_t *ret) { + _cleanup_(sd_device_unrefp) sd_device *pci = NULL; + _cleanup_closedir_ DIR *dir = NULL; + int r; + + assert(dev); + assert(ret); + + /* ACPI _SUN — slot user number */ + r = sd_device_new_from_subsystem_sysname(&pci, "subsystem", "pci"); + if (r < 0) + return log_debug_errno(r, "Failed to create sd_device object for pci subsystem: %m"); + + r = device_opendir(pci, "slots", &dir); + if (r < 0) + return log_device_debug_errno(dev, r, "Cannot open 'slots' subdirectory: %m"); + + for (sd_device *slot_dev = dev; slot_dev; ) { + uint32_t slot = 0; /* avoid false maybe-uninitialized warning */ + + r = parse_hotplug_slot_from_function_id(slot_dev, dirfd(dir), &slot); + if (r < 0) + return r; + if (r > 0) { + *ret = slot; + return 1; /* domain should be ignored. */ + } + + r = pci_get_hotplug_slot_from_address(slot_dev, pci, dir, &slot); + if (r < 0) + return r; + if (r > 0) { + /* We found the match between PCI device and slot. However, we won't use the slot + * index if the device is a PCI bridge, because it can have other child devices that + * will try to claim the same index and that would create name collision. */ + if (naming_scheme_has(NAMING_BRIDGE_NO_SLOT) && is_pci_bridge(slot_dev)) { + if (naming_scheme_has(NAMING_BRIDGE_MULTIFUNCTION_SLOT) && is_pci_multifunction(dev) <= 0) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ESTALE), + "Not using slot information because the PCI device associated with " + "the hotplug slot is a bridge and the PCI device has a single function."); + + if (!naming_scheme_has(NAMING_BRIDGE_MULTIFUNCTION_SLOT)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ESTALE), + "Not using slot information because the PCI device is a bridge."); + } + + *ret = slot; + return 0; /* domain can be still used. */ + } + + if (sd_device_get_parent_with_subsystem_devtype(slot_dev, "pci", NULL, &slot_dev) < 0) + break; + } + + return -ENOENT; +} + +static int get_pci_slot_specifiers( + sd_device *dev, + char **ret_domain, + char **ret_bus_and_slot, + char **ret_func) { + + _cleanup_free_ char *domain_spec = NULL, *bus_and_slot_spec = NULL, *func_spec = NULL; + unsigned domain, bus, slot, func; + const char *sysname; + int r; + + assert(dev); + assert(ret_domain); + assert(ret_bus_and_slot); + assert(ret_func); + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + r = sscanf(sysname, "%x:%x:%x.%u", &domain, &bus, &slot, &func); + if (r != 4) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "Failed to parse slot information from PCI device sysname."); + + if (naming_scheme_has(NAMING_NPAR_ARI) && + is_pci_ari_enabled(dev)) + /* ARI devices support up to 256 functions on a single device ("slot"), and interpret the + * traditional 5-bit slot and 3-bit function number as a single 8-bit function number, + * where the slot makes up the upper 5 bits. */ + func += slot * 8; + + if (domain > 0 && asprintf(&domain_spec, "P%u", domain) < 0) + return log_oom_debug(); + + if (asprintf(&bus_and_slot_spec, "p%us%u", bus, slot) < 0) + return log_oom_debug(); + + if ((func > 0 || is_pci_multifunction(dev) > 0) && + asprintf(&func_spec, "f%u", func) < 0) + return log_oom_debug(); + + *ret_domain = TAKE_PTR(domain_spec); + *ret_bus_and_slot = TAKE_PTR(bus_and_slot_spec); + *ret_func = TAKE_PTR(func_spec); + return 0; +} + +static int names_pci_slot(sd_device *dev, sd_device *pci_dev, const char *prefix, const char *suffix, bool test) { + _cleanup_free_ char *domain = NULL, *bus_and_slot = NULL, *func = NULL, *port = NULL; + uint32_t hotplug_slot = 0; /* avoid false maybe-uninitialized warning */ + char str[ALTIFNAMSIZ]; + int r; + + assert(dev); + assert(pci_dev); + assert(prefix); + + r = get_pci_slot_specifiers(pci_dev, &domain, &bus_and_slot, &func); + if (r < 0) + return r; + + r = get_port_specifier(dev, /* fallback_to_dev_id = */ true, &port); + if (r < 0) + return r; + + /* compose a name based on the raw kernel's PCI bus, slot numbers */ + if (snprintf_ok(str, sizeof str, "%s%s%s%s%s%s", + prefix, strempty(domain), bus_and_slot, strempty(func), strempty(port), strempty(suffix))) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + + log_device_debug(dev, "PCI path identifier: domain=%s bus_and_slot=%s func=%s port=%s %s %s", + strna(domain), bus_and_slot, strna(func), strna(port), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), empty_to_na(str)); + + r = pci_get_hotplug_slot(pci_dev, &hotplug_slot); + if (r < 0) + return r; + if (r > 0) + /* If the hotplug slot is found through the function ID, then drop the domain from the name. + * See comments in parse_hotplug_slot_from_function_id(). */ + domain = mfree(domain); + + if (snprintf_ok(str, sizeof str, "%s%ss%"PRIu32"%s%s%s", + prefix, strempty(domain), hotplug_slot, strempty(func), strempty(port), strempty(suffix))) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + + log_device_debug(dev, "Slot identifier: domain=%s slot=%"PRIu32" func=%s port=%s %s %s", + strna(domain), hotplug_slot, strna(func), strna(port), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), empty_to_na(str)); + + return 0; +} + +static int names_vio(sd_device *dev, const char *prefix, bool test) { + _cleanup_free_ char *s = NULL; + unsigned slotid; + int r; + + assert(dev); + assert(prefix); + + /* get ibmveth/ibmvnic slot-based names. */ + + /* check if our direct parent is a VIO device with no other bus in-between */ + if (get_matching_parent(dev, STRV_MAKE("vio"), /* skip_virtio = */ false, NULL) < 0) + return 0; + + log_device_debug(dev, "Parent device is in the vio subsystem."); + + /* The devices' $DEVPATH number is tied to (virtual) hardware (slot id + * selected in the HMC), thus this provides a reliable naming (e.g. + * "/devices/vio/30000002/net/eth1"); we ignore the bus number, as + * there should only ever be one bus, and then remove leading zeros. */ + r = get_first_syspath_component(dev, "/sys/devices/vio/", &s); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get VIO bus ID and slot ID: %m"); + + if (r != 8) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "VIO bus ID and slot ID have invalid length: %s", s); + + if (!in_charset(s, HEXDIGITS)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "VIO bus ID and slot ID contain invalid characters: %s", s); + + /* Parse only slot ID (the last 4 hexdigits). */ + r = safe_atou_full(s + 4, 16, &slotid); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to parse VIO slot from '%s': %m", s); + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%sv%u", prefix, slotid)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + log_device_debug(dev, "Vio slot identifier: slotid=%u %s %s", + slotid, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + return 0; +} + +static int names_platform(sd_device *dev, const char *prefix, bool test) { + _cleanup_free_ char *p = NULL; + const char *validchars; + char *vendor, *model_str, *instance_str; + unsigned model, instance; + int r; + + assert(dev); + assert(prefix); + + /* get ACPI path names for ARM64 platform devices */ + + /* check if our direct parent is a platform device with no other bus in-between */ + if (get_matching_parent(dev, STRV_MAKE("platform"), /* skip_virtio = */ false, NULL) < 0) + return 0; + + log_device_debug(dev, "Parent device is in the platform subsystem."); + + r = get_first_syspath_component(dev, "/sys/devices/platform/", &p); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get platform ID: %m"); + + /* Platform devices are named after ACPI table match, and instance id + * eg. "/sys/devices/platform/HISI00C2:00" + * The Vendor (3 or 4 char), followed by hexadecimal model number : instance id. */ + if (r == 10 && p[7] == ':') { + /* 3 char vendor string */ + vendor = strndupa(p, 3); + model_str = strndupa(p + 3, 4); + instance_str = strndupa(p + 8, 2); + validchars = UPPERCASE_LETTERS; + } else if (r == 11 && p[8] == ':') { + /* 4 char vendor string */ + vendor = strndupa(p, 4); + model_str = strndupa(p + 4, 4); + instance_str = strndupa(p + 9, 2); + validchars = UPPERCASE_LETTERS DIGITS; + } else + return -EOPNOTSUPP; + + if (!in_charset(vendor, validchars)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENOENT), + "Platform vendor contains invalid characters: %s", vendor); + + ascii_strlower(vendor); + + r = safe_atou_full(model_str, 16, &model); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to parse model number \"%s\": %m", model_str); + + r = safe_atou_full(instance_str, 16, &instance); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to parse instance id \"%s\": %m", instance_str); + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%sa%s%xi%u", prefix, vendor, model, instance)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + log_device_debug(dev, "Platform identifier: vendor=%s model=%x instance=%u %s %s", + vendor, model, instance, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + return 0; +} + +static int names_devicetree(sd_device *dev, const char *prefix, bool test) { + _cleanup_(sd_device_unrefp) sd_device *aliases_dev = NULL, *ofnode_dev = NULL, *devicetree_dev = NULL; + const char *ofnode_path, *ofnode_syspath, *devicetree_syspath; + sd_device *parent; + int r; + + assert(dev); + assert(prefix); + + if (!naming_scheme_has(NAMING_DEVICETREE_ALIASES)) + return 0; + + /* only ethernet supported for now */ + if (!streq(prefix, "en")) + return -EOPNOTSUPP; + + /* check if our direct parent has an of_node */ + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get parent device: %m"); + + r = sd_device_new_child(&ofnode_dev, parent, "of_node"); + if (r < 0) + return log_device_debug_errno(parent, r, "Failed to get 'of_node' child device: %m"); + + r = sd_device_get_syspath(ofnode_dev, &ofnode_syspath); + if (r < 0) + return log_device_debug_errno(ofnode_dev, r, "Failed to get syspath: %m"); + + /* /proc/device-tree should be a symlink to /sys/firmware/devicetree/base. */ + r = sd_device_new_from_path(&devicetree_dev, "/proc/device-tree"); + if (r < 0) + return log_debug_errno(r, "Failed to create sd-device object from '/proc/device-tree': %m"); + + r = sd_device_get_syspath(devicetree_dev, &devicetree_syspath); + if (r < 0) + return log_device_debug_errno(devicetree_dev, r, "Failed to get syspath: %m"); + + /* + * Example paths: + * devicetree_syspath = /sys/firmware/devicetree/base + * ofnode_syspath = /sys/firmware/devicetree/base/soc/ethernet@deadbeef + * ofnode_path = soc/ethernet@deadbeef + */ + ofnode_path = path_startswith(ofnode_syspath, devicetree_syspath); + if (!ofnode_path) + return log_device_debug_errno(ofnode_dev, SYNTHETIC_ERRNO(EINVAL), + "The device '%s' is not a child device of '%s': %m", + ofnode_syspath, devicetree_syspath); + + /* Get back our leading / to match the contents of the aliases */ + ofnode_path--; + assert(path_is_absolute(ofnode_path)); + + r = sd_device_new_child(&aliases_dev, devicetree_dev, "aliases"); + if (r < 0) + return log_device_debug_errno(devicetree_dev, r, + "Failed to get 'aliases' child device: %m"); + + FOREACH_DEVICE_SYSATTR(aliases_dev, alias) { + const char *alias_path, *alias_index, *conflict; + unsigned i; + + alias_index = startswith(alias, "ethernet"); + if (!alias_index) + continue; + + if (sd_device_get_sysattr_value(aliases_dev, alias, &alias_path) < 0) + continue; + + if (!path_equal(ofnode_path, alias_path)) + continue; + + /* If there's no index, we default to 0... */ + if (isempty(alias_index)) { + i = 0; + conflict = "ethernet0"; + } else { + r = safe_atou(alias_index, &i); + if (r < 0) + return log_device_debug_errno(dev, r, + "Could not get index of alias %s: %m", alias); + conflict = "ethernet"; + } + + /* ...but make sure we don't have an alias conflict */ + if (i == 0 && sd_device_get_sysattr_value(aliases_dev, conflict, NULL) >= 0) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EEXIST), + "Ethernet alias conflict: ethernet and ethernet0 both exist"); + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%sd%u", prefix, i)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_ONBOARD", str); + log_device_debug(dev, "devicetree identifier: alias_index=%u %s \"%s\"", + i, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + return 0; + } + + return -ENOENT; +} + +static int names_pci(sd_device *dev, const char *prefix, bool test) { + _cleanup_(sd_device_unrefp) sd_device *physfn_pcidev = NULL; + _cleanup_free_ char *virtfn_suffix = NULL; + sd_device *parent; + + assert(dev); + assert(prefix); + + /* check if our direct parent is a PCI device with no other bus in-between */ + if (get_matching_parent(dev, STRV_MAKE("pci"), /* skip_virtio = */ true, &parent) < 0) + return 0; + + /* If this is an SR-IOV virtual device, get base name using physical device and add virtfn suffix. */ + if (naming_scheme_has(NAMING_SR_IOV_V) && + get_virtfn_info(parent, &physfn_pcidev, &virtfn_suffix) >= 0) + parent = physfn_pcidev; + else + (void) names_pci_onboard_label(dev, parent, prefix, test); + + (void) names_pci_onboard(dev, parent, prefix, virtfn_suffix, test); + (void) names_pci_slot(dev, parent, prefix, virtfn_suffix, test); + return 0; +} + +static int get_usb_specifier(sd_device *dev, char **ret) { + char *ports, *config, *interf, *s, *buf; + const char *sysname; + int r; + + assert(dev); + assert(ret); + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + /* get USB port number chain, configuration, interface */ + s = strchr(sysname, '-'); + if (!s) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "sysname \"%s\" does not have '-' in the expected place.", sysname); + + ports = strdupa_safe(s + 1); + s = strchr(ports, ':'); + if (!s) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "sysname \"%s\" does not have ':' in the expected place.", sysname); + + *s = '\0'; + config = s + 1; + s = strchr(config, '.'); + if (!s) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "sysname \"%s\" does not have '.' in the expected place.", sysname); + + *s = '\0'; + interf = s + 1; + + /* prefix every port number in the chain with "u" */ + string_replace_char(ports, '.', 'u'); + + /* suppress the common config == 1 */ + if (streq(config, "1")) + config = NULL; + + /* suppress the interface == 0 */ + if (streq(interf, "0")) + interf = NULL; + + buf = strjoin("u", ports, + config ? "c" : "", strempty(config), + interf ? "i" : "", strempty(interf)); + if (!buf) + return log_oom_debug(); + + log_device_debug(dev, "USB name identifier: ports=%s config=%s interface=%s %s %s", + ports, strna(config), strna(interf), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), buf); + + *ret = buf; + return 0; +} + +static int names_usb(sd_device *dev, const char *prefix, bool test) { + _cleanup_free_ char *suffix = NULL; + sd_device *usbdev, *pcidev; + int r; + + assert(dev); + assert(prefix); + + /* USB device */ + + r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &usbdev); + if (r < 0) + return log_device_debug_errno(dev, r, "Could not find usb parent device: %m"); + + r = get_usb_specifier(usbdev, &suffix); + if (r < 0) + return r; + + /* If the USB bus is on PCI bus, then suffix the USB specifier to the name based on the PCI bus. */ + r = sd_device_get_parent_with_subsystem_devtype(usbdev, "pci", NULL, &pcidev); + if (r >= 0) + return names_pci_slot(dev, pcidev, prefix, suffix, test); + + if (r != -ENOENT || !naming_scheme_has(NAMING_USB_HOST)) + return log_device_debug_errno(usbdev, r, "Failed to get parent PCI bus: %m"); + + /* Otherwise, e.g. on-chip asics that have USB ports, use the USB specifier as is. */ + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%s%s", prefix, suffix)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + + return 0; +} + +static int get_bcma_specifier(sd_device *dev, char **ret) { + const char *sysname; + char *buf = NULL; + unsigned core; + int r; + + assert(dev); + assert(ret); + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + /* bus num:core num */ + r = sscanf(sysname, "bcma%*u:%u", &core); + if (r != 1) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "Failed to parse bcma device information."); + + /* suppress the common core == 0 */ + if (core > 0 && asprintf(&buf, "b%u", core) < 0) + return log_oom_debug(); + + log_device_debug(dev, "BCMA core identifier: core=%u %s \"%s\"", + core, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), strna(buf)); + + *ret = buf; + return 0; +} + +static int names_bcma(sd_device *dev, const char *prefix, bool test) { + _cleanup_free_ char *suffix = NULL; + sd_device *bcmadev, *pcidev; + int r; + + assert(dev); + assert(prefix); + + r = sd_device_get_parent_with_subsystem_devtype(dev, "bcma", NULL, &bcmadev); + if (r < 0) + return log_device_debug_errno(dev, r, "Could not get bcma parent device: %m"); + + r = sd_device_get_parent_with_subsystem_devtype(bcmadev, "pci", NULL, &pcidev); + if (r < 0) + return log_device_debug_errno(dev, r, "Could not get pci parent device: %m"); + + r = get_bcma_specifier(bcmadev, &suffix); + if (r < 0) + return r; + + return names_pci_slot(dev, pcidev, prefix, suffix, test); +} + +static int names_ccw(sd_device *dev, const char *prefix, bool test) { + sd_device *cdev; + const char *bus_id; + size_t bus_id_start, bus_id_len; + int r; + + assert(dev); + assert(prefix); + + /* get path names for Linux on System z network devices */ + + if (get_matching_parent(dev, STRV_MAKE("ccwgroup", "ccw"), /* skip_virtio = */ true, &cdev) < 0) + return 0; + + log_device_debug(dev, "Device is CCW."); + + /* Retrieve bus-ID of the CCW device. The bus-ID uniquely + * identifies the network device on the Linux on System z channel + * subsystem. Note that the bus-ID contains lowercase characters. + */ + r = sd_device_get_sysname(cdev, &bus_id); + if (r < 0) + return log_device_debug_errno(cdev, r, "Failed to get sysname: %m"); + + /* Check the length of the bus-ID. Rely on the fact that the kernel provides a correct bus-ID; + * alternatively, improve this check and parse and verify each bus-ID part... + */ + bus_id_len = strlen(bus_id); + if (!IN_SET(bus_id_len, 8, 9)) + return log_device_debug_errno(cdev, SYNTHETIC_ERRNO(EINVAL), "Invalid bus_id: %s", bus_id); + + /* Strip leading zeros from the bus id for aesthetic purposes. This + * keeps the ccw names stable, yet much shorter in general case of + * bus_id 0.0.0600 -> 600. This is similar to e.g. how PCI domain is + * not prepended when it is zero. Preserve the last 0 for 0.0.0000. + */ + bus_id_start = strspn(bus_id, ".0"); + bus_id += bus_id_start < bus_id_len ? bus_id_start : bus_id_len - 1; + + /* Use the CCW bus-ID as network device name */ + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%sc%s", prefix, bus_id)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + log_device_debug(dev, "CCW identifier: ccw_busid=%s %s \"%s\"", + bus_id, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + return 0; +} + +/* IEEE Organizationally Unique Identifier vendor string */ +static int ieee_oui(sd_device *dev, const struct hw_addr_data *hw_addr, bool test) { + char str[32]; + + assert(dev); + assert(hw_addr); + + if (hw_addr->length != 6) + return -EOPNOTSUPP; + + /* skip commonly misused 00:00:00 (Xerox) prefix */ + if (hw_addr->bytes[0] == 0 && + hw_addr->bytes[1] == 0 && + hw_addr->bytes[2] == 0) + return -EINVAL; + + xsprintf(str, "OUI:%02X%02X%02X%02X%02X%02X", + hw_addr->bytes[0], + hw_addr->bytes[1], + hw_addr->bytes[2], + hw_addr->bytes[3], + hw_addr->bytes[4], + hw_addr->bytes[5]); + + return udev_builtin_hwdb_lookup(dev, NULL, str, NULL, test); +} + +static int names_mac(sd_device *dev, const char *prefix, bool test) { + unsigned iftype, assign_type; + struct hw_addr_data hw_addr; + const char *s; + int r; + + assert(dev); + assert(prefix); + + r = device_get_sysattr_unsigned(dev, "type", &iftype); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to read 'type' attribute: %m"); + + /* The persistent part of a hardware address of an InfiniBand NIC is 8 bytes long. We cannot + * fit this much in an iface name. + * TODO: but it can be used as alternative names?? */ + if (iftype == ARPHRD_INFINIBAND) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP), + "Not generating MAC name for infiniband device."); + + /* check for NET_ADDR_PERM, skip random MAC addresses */ + r = device_get_sysattr_unsigned(dev, "addr_assign_type", &assign_type); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to read/parse addr_assign_type: %m"); + + if (assign_type != NET_ADDR_PERM) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "addr_assign_type=%u, MAC address is not permanent.", assign_type); + + r = sd_device_get_sysattr_value(dev, "address", &s); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to read 'address' attribute: %m"); + + r = parse_hw_addr(s, &hw_addr); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to parse 'address' attribute: %m"); + + if (hw_addr.length != 6) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP), + "Not generating MAC name for device with MAC address of length %zu.", + hw_addr.length); + + char str[ALTIFNAMSIZ]; + xsprintf(str, "%sx%s", prefix, HW_ADDR_TO_STR_FULL(&hw_addr, HW_ADDR_TO_STRING_NO_COLON)); + udev_builtin_add_property(dev, test, "ID_NET_NAME_MAC", str); + log_device_debug(dev, "MAC address identifier: hw_addr=%s %s %s", + HW_ADDR_TO_STR(&hw_addr), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + + (void) ieee_oui(dev, &hw_addr, test); + return 0; +} + +static int names_netdevsim(sd_device *dev, const char *prefix, bool test) { + sd_device *netdevsimdev; + const char *sysnum, *phys_port_name; + unsigned addr; + int r; + + assert(dev); + assert(prefix); + + /* get netdevsim path names */ + + if (!naming_scheme_has(NAMING_NETDEVSIM)) + return 0; + + r = sd_device_get_parent_with_subsystem_devtype(dev, "netdevsim", NULL, &netdevsimdev); + if (r < 0) + return r; + + r = sd_device_get_sysnum(netdevsimdev, &sysnum); + if (r < 0) + return log_device_debug_errno(netdevsimdev, r, "Failed to get device sysnum: %m"); + + r = safe_atou(sysnum, &addr); + if (r < 0) + return log_device_debug_errno(netdevsimdev, r, "Failed to parse device sysnum: %m"); + + r = sd_device_get_sysattr_value(dev, "phys_port_name", &phys_port_name); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get 'phys_port_name' attribute: %m"); + if (isempty(phys_port_name)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP), + "The 'phys_port_name' attribute is empty."); + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%si%un%s", prefix, addr, phys_port_name)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + log_device_debug(dev, "Netdevsim identifier: address=%u, port_name=%s %s %s", + addr, phys_port_name, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + return 0; +} + +static int names_xen(sd_device *dev, const char *prefix, bool test) { + _cleanup_free_ char *vif = NULL; + const char *p; + unsigned id; + int r; + + assert(dev); + assert(prefix); + + /* get xen vif "slot" based names. */ + + if (!naming_scheme_has(NAMING_XEN_VIF)) + return 0; + + /* check if our direct parent is a Xen VIF device with no other bus in-between */ + if (get_matching_parent(dev, STRV_MAKE("xen"), /* skip_virtio = */ false, NULL) < 0) + return 0; + + /* Use the vif-n name to extract "n" */ + r = get_first_syspath_component(dev, "/sys/devices/", &vif); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get Xen VIF name: %m"); + + p = startswith(vif, "vif-"); + if (!p) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid vif name: %s: %m", vif); + + r = safe_atou_full(p, SAFE_ATO_REFUSE_PLUS_MINUS | SAFE_ATO_REFUSE_LEADING_ZERO | + SAFE_ATO_REFUSE_LEADING_WHITESPACE | 10, &id); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to parse vif index from '%s': %m", p); + + char str[ALTIFNAMSIZ]; + if (snprintf_ok(str, sizeof str, "%sX%u", prefix, id)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + log_device_debug(dev, "Xen identifier: id=%u %s %s", + id, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix)); + return 0; +} + +static int get_ifname_prefix(sd_device *dev, const char **ret) { + unsigned iftype; + int r; + + assert(dev); + assert(ret); + + r = device_get_sysattr_unsigned(dev, "type", &iftype); + if (r < 0) + return r; + + /* handle only ARPHRD_ETHER, ARPHRD_SLIP and ARPHRD_INFINIBAND devices */ + switch (iftype) { + case ARPHRD_ETHER: { + const char *s = NULL; + + r = sd_device_get_devtype(dev, &s); + if (r < 0 && r != -ENOENT) + return r; + + if (streq_ptr(s, "wlan")) + *ret = "wl"; + else if (streq_ptr(s, "wwan")) + *ret = "ww"; + else + *ret = "en"; + return 0; + } + case ARPHRD_INFINIBAND: + if (!naming_scheme_has(NAMING_INFINIBAND)) + return -EOPNOTSUPP; + + *ret = "ib"; + return 0; + + case ARPHRD_SLIP: + *ret = "sl"; + return 0; + + default: + return -EOPNOTSUPP; + } +} + +static int device_is_stacked(sd_device *dev) { + int ifindex, iflink, r; + + assert(dev); + + r = sd_device_get_ifindex(dev, &ifindex); + if (r < 0) + return r; + + r = device_get_sysattr_int(dev, "iflink", &iflink); + if (r < 0) + return r; + + return ifindex != iflink; +} + +static int builtin_net_id(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + const char *prefix; + int r; + + /* skip stacked devices, like VLANs, ... */ + r = device_is_stacked(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to check if the device is stacked: %m"); + if (r > 0) + return 0; + + r = get_ifname_prefix(dev, &prefix); + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to determine prefix for network interface naming, ignoring: %m"); + return 0; + } + + udev_builtin_add_property(dev, test, "ID_NET_NAMING_SCHEME", naming_scheme()->name); + + (void) names_mac(dev, prefix, test); + (void) names_devicetree(dev, prefix, test); + (void) names_ccw(dev, prefix, test); + (void) names_vio(dev, prefix, test); + (void) names_platform(dev, prefix, test); + (void) names_netdevsim(dev, prefix, test); + (void) names_xen(dev, prefix, test); + (void) names_pci(dev, prefix, test); + (void) names_usb(dev, prefix, test); + (void) names_bcma(dev, prefix, test); + + return 0; +} + +static int builtin_net_id_init(void) { + /* Load naming scheme here to suppress log messages in workers. */ + naming_scheme(); + return 0; +} + +const UdevBuiltin udev_builtin_net_id = { + .name = "net_id", + .cmd = builtin_net_id, + .init = builtin_net_id_init, + .help = "Network device properties", +}; diff --git a/src/udev/udev-builtin-net_setup_link.c b/src/udev/udev-builtin-net_setup_link.c new file mode 100644 index 0000000..a308a21 --- /dev/null +++ b/src/udev/udev-builtin-net_setup_link.c @@ -0,0 +1,113 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "device-util.h" +#include "escape.h" +#include "errno-util.h" +#include "link-config.h" +#include "log.h" +#include "string-util.h" +#include "strv.h" +#include "udev-builtin.h" + +static LinkConfigContext *ctx = NULL; + +static int builtin_net_setup_link(UdevEvent *event, int argc, char **argv, bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + _cleanup_(link_freep) Link *link = NULL; + _cleanup_free_ char *joined = NULL; + int r; + + if (argc > 1) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + r = link_new(ctx, &event->rtnl, dev, &link); + if (r == -ENODEV) { + log_device_debug_errno(dev, r, "Link vanished while getting information, ignoring."); + return 0; + } + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get link information: %m"); + + r = link_get_config(ctx, link); + if (r < 0) { + if (r == -ENOENT) { + log_device_debug_errno(dev, r, "No matching link configuration found, ignoring device."); + return 0; + } + + return log_device_error_errno(dev, r, "Failed to get link config: %m"); + } + + r = link_apply_config(ctx, &event->rtnl, link); + if (r == -ENODEV) + log_device_debug_errno(dev, r, "Link vanished while applying configuration, ignoring."); + else if (r < 0) + log_device_warning_errno(dev, r, "Could not apply link configuration, ignoring: %m"); + + udev_builtin_add_property(dev, test, "ID_NET_LINK_FILE", link->config->filename); + if (link->new_name) + udev_builtin_add_property(dev, test, "ID_NET_NAME", link->new_name); + + event->altnames = TAKE_PTR(link->altnames); + + STRV_FOREACH(d, link->config->dropins) { + _cleanup_free_ char *escaped = NULL; + + escaped = xescape(*d, ":"); + if (!escaped) + return log_oom(); + + if (!strextend_with_separator(&joined, ":", escaped)) + return log_oom(); + } + + udev_builtin_add_property(dev, test, "ID_NET_LINK_FILE_DROPINS", joined); + + return 0; +} + +static int builtin_net_setup_link_init(void) { + int r; + + if (ctx) + return 0; + + r = link_config_ctx_new(&ctx); + if (r < 0) + return r; + + r = link_config_load(ctx); + if (r < 0) + return r; + + log_debug("Created link configuration context."); + return 0; +} + +static void builtin_net_setup_link_exit(void) { + ctx = link_config_ctx_free(ctx); + log_debug("Unloaded link configuration context."); +} + +static bool builtin_net_setup_link_should_reload(void) { + if (!ctx) + return false; + + if (link_config_should_reload(ctx)) { + log_debug("Link configuration context needs reloading."); + return true; + } + + return false; +} + +const UdevBuiltin udev_builtin_net_setup_link = { + .name = "net_setup_link", + .cmd = builtin_net_setup_link, + .init = builtin_net_setup_link_init, + .exit = builtin_net_setup_link_exit, + .should_reload = builtin_net_setup_link_should_reload, + .help = "Configure network link", + .run_once = false, +}; diff --git a/src/udev/udev-builtin-path_id.c b/src/udev/udev-builtin-path_id.c new file mode 100644 index 0000000..467c9a6 --- /dev/null +++ b/src/udev/udev-builtin-path_id.c @@ -0,0 +1,896 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * compose persistent device path + * + * Logic based on Hannes Reinecke's shell script. + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <linux/usb/ch11.h> +#include <stdarg.h> +#include <stdio.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "strv.h" +#include "sysexits.h" +#include "udev-builtin.h" +#include "udev-util.h" + +_printf_(2,3) +static void path_prepend(char **path, const char *fmt, ...) { + va_list va; + _cleanup_free_ char *pre = NULL; + int r; + + va_start(va, fmt); + r = vasprintf(&pre, fmt, va); + va_end(va); + if (r < 0) { + log_oom(); + exit(EX_OSERR); + } + + if (*path) { + char *new; + + new = strjoin(pre, "-", *path); + if (!new) { + log_oom(); + exit(EX_OSERR); + } + + free_and_replace(*path, new); + } else + *path = TAKE_PTR(pre); +} + +/* +** Linux only supports 32 bit luns. +** See drivers/scsi/scsi_scan.c::scsilun_to_int() for more details. +*/ +static int format_lun_number(sd_device *dev, char **path) { + const char *sysnum; + unsigned long lun; + int r; + + r = sd_device_get_sysnum(dev, &sysnum); + if (r < 0) + return r; + if (!sysnum) + return -ENOENT; + + r = safe_atolu_full(sysnum, 10, &lun); + if (r < 0) + return r; + if (lun < 256) + /* address method 0, peripheral device addressing with bus id of zero */ + path_prepend(path, "lun-%lu", lun); + else + /* handle all other lun addressing methods by using a variant of the original lun format */ + path_prepend(path, "lun-0x%04lx%04lx00000000", lun & 0xffff, (lun >> 16) & 0xffff); + + return 0; +} + +static sd_device *skip_subsystem(sd_device *dev, const char *subsys) { + sd_device *parent; + + assert(dev); + assert(subsys); + + /* Unlike the function name, this drops multiple parent devices EXCEPT FOR THE LAST ONE. + * The last one will be dropped at the end of the loop in builtin_path_id(). + * E.g. + * Input: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0 + * Output: /sys/devices/pci0000:00/0000:00:14.0/usb1 + */ + + for (parent = dev; ; ) { + const char *subsystem; + + if (sd_device_get_subsystem(parent, &subsystem) < 0) + break; + + if (!streq(subsystem, subsys)) + break; + + dev = parent; + if (sd_device_get_parent(dev, &parent) < 0) + break; + } + + return dev; +} + +static sd_device *handle_scsi_fibre_channel(sd_device *parent, char **path) { + sd_device *targetdev; + _cleanup_(sd_device_unrefp) sd_device *fcdev = NULL; + const char *port, *sysname; + _cleanup_free_ char *lun = NULL; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) + return NULL; + if (sd_device_get_sysname(targetdev, &sysname) < 0) + return NULL; + if (sd_device_new_from_subsystem_sysname(&fcdev, "fc_transport", sysname) < 0) + return NULL; + if (sd_device_get_sysattr_value(fcdev, "port_name", &port) < 0) + return NULL; + + format_lun_number(parent, &lun); + path_prepend(path, "fc-%s-%s", port, lun); + return parent; +} + +static sd_device *handle_scsi_sas_wide_port(sd_device *parent, char **path) { + sd_device *targetdev, *target_parent; + _cleanup_(sd_device_unrefp) sd_device *sasdev = NULL; + const char *sas_address, *sysname; + _cleanup_free_ char *lun = NULL; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) + return NULL; + if (sd_device_get_parent(targetdev, &target_parent) < 0) + return NULL; + if (sd_device_get_sysname(target_parent, &sysname) < 0) + return NULL; + if (sd_device_new_from_subsystem_sysname(&sasdev, "sas_device", sysname) < 0) + return NULL; + if (sd_device_get_sysattr_value(sasdev, "sas_address", &sas_address) < 0) + return NULL; + + format_lun_number(parent, &lun); + path_prepend(path, "sas-%s-%s", sas_address, lun); + return parent; +} + +static sd_device *handle_scsi_sas(sd_device *parent, char **path) { + sd_device *targetdev, *target_parent, *port, *expander; + _cleanup_(sd_device_unrefp) sd_device *target_sasdev = NULL, *expander_sasdev = NULL, *port_sasdev = NULL; + const char *sas_address = NULL; + const char *phy_id; + const char *phy_count, *sysname; + _cleanup_free_ char *lun = NULL; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) + return NULL; + if (sd_device_get_parent(targetdev, &target_parent) < 0) + return NULL; + if (sd_device_get_sysname(target_parent, &sysname) < 0) + return NULL; + /* Get sas device */ + if (sd_device_new_from_subsystem_sysname(&target_sasdev, "sas_device", sysname) < 0) + return NULL; + /* The next parent is sas port */ + if (sd_device_get_parent(target_parent, &port) < 0) + return NULL; + if (sd_device_get_sysname(port, &sysname) < 0) + return NULL; + /* Get port device */ + if (sd_device_new_from_subsystem_sysname(&port_sasdev, "sas_port", sysname) < 0) + return NULL; + if (sd_device_get_sysattr_value(port_sasdev, "num_phys", &phy_count) < 0) + return NULL; + + /* Check if we are simple disk */ + if (strncmp(phy_count, "1", 2) != 0) + return handle_scsi_sas_wide_port(parent, path); + + /* Get connected phy */ + if (sd_device_get_sysattr_value(target_sasdev, "phy_identifier", &phy_id) < 0) + return NULL; + + /* The port's parent is either hba or expander */ + if (sd_device_get_parent(port, &expander) < 0) + return NULL; + + if (sd_device_get_sysname(expander, &sysname) < 0) + return NULL; + /* Get expander device */ + if (sd_device_new_from_subsystem_sysname(&expander_sasdev, "sas_device", sysname) >= 0) { + /* Get expander's address */ + if (sd_device_get_sysattr_value(expander_sasdev, "sas_address", &sas_address) < 0) + return NULL; + } + + format_lun_number(parent, &lun); + if (sas_address) + path_prepend(path, "sas-exp%s-phy%s-%s", sas_address, phy_id, lun); + else + path_prepend(path, "sas-phy%s-%s", phy_id, lun); + + return parent; +} + +static sd_device *handle_scsi_iscsi(sd_device *parent, char **path) { + sd_device *transportdev; + _cleanup_(sd_device_unrefp) sd_device *sessiondev = NULL, *conndev = NULL; + const char *target, *connname, *addr, *port; + _cleanup_free_ char *lun = NULL; + const char *sysname, *sysnum; + + assert(parent); + assert(path); + + /* find iscsi session */ + for (transportdev = parent; ; ) { + + if (sd_device_get_parent(transportdev, &transportdev) < 0) + return NULL; + if (sd_device_get_sysname(transportdev, &sysname) < 0) + return NULL; + if (startswith(sysname, "session")) + break; + } + + /* find iscsi session device */ + if (sd_device_new_from_subsystem_sysname(&sessiondev, "iscsi_session", sysname) < 0) + return NULL; + + if (sd_device_get_sysattr_value(sessiondev, "targetname", &target) < 0) + return NULL; + + if (sd_device_get_sysnum(transportdev, &sysnum) < 0 || !sysnum) + return NULL; + connname = strjoina("connection", sysnum, ":0"); + if (sd_device_new_from_subsystem_sysname(&conndev, "iscsi_connection", connname) < 0) + return NULL; + + if (sd_device_get_sysattr_value(conndev, "persistent_address", &addr) < 0) + return NULL; + if (sd_device_get_sysattr_value(conndev, "persistent_port", &port) < 0) + return NULL; + + format_lun_number(parent, &lun); + path_prepend(path, "ip-%s:%s-iscsi-%s-%s", addr, port, target, lun); + return parent; +} + +static sd_device *handle_scsi_ata(sd_device *parent, char **path, char **compat_path) { + sd_device *targetdev, *target_parent; + _cleanup_(sd_device_unrefp) sd_device *atadev = NULL; + const char *port_no, *sysname, *name; + unsigned host, bus, target, lun; + + assert(parent); + assert(path); + + if (sd_device_get_sysname(parent, &name) < 0) + return NULL; + if (sscanf(name, "%u:%u:%u:%u", &host, &bus, &target, &lun) != 4) + return NULL; + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &targetdev) < 0) + return NULL; + + if (sd_device_get_parent(targetdev, &target_parent) < 0) + return NULL; + + if (sd_device_get_sysname(target_parent, &sysname) < 0) + return NULL; + if (sd_device_new_from_subsystem_sysname(&atadev, "ata_port", sysname) < 0) + return NULL; + + if (sd_device_get_sysattr_value(atadev, "port_no", &port_no) < 0) + return NULL; + + if (bus != 0) + /* Devices behind port multiplier have a bus != 0 */ + path_prepend(path, "ata-%s.%u.0", port_no, bus); + else + /* Master/slave are distinguished by target id */ + path_prepend(path, "ata-%s.%u", port_no, target); + + /* old compatible persistent link for ATA devices */ + if (compat_path) + path_prepend(compat_path, "ata-%s", port_no); + + return parent; +} + +static sd_device *handle_scsi_default(sd_device *parent, char **path) { + sd_device *hostdev; + int host, bus, target, lun; + const char *name, *base, *pos; + _cleanup_closedir_ DIR *dir = NULL; + int basenum = -1; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0) + return NULL; + + if (sd_device_get_sysname(parent, &name) < 0) + return NULL; + if (sscanf(name, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4) + return NULL; + + /* + * Rebase host offset to get the local relative number + * + * Note: This is by definition racy, unreliable and too simple. + * Please do not copy this model anywhere. It's just a left-over + * from the time we had no idea how things should look like in + * the end. + * + * Making assumptions about a global in-kernel counter and use + * that to calculate a local offset is a very broken concept. It + * can only work as long as things are in strict order. + * + * The kernel needs to export the instance/port number of a + * controller directly, without the need for rebase magic like + * this. Manual driver unbind/bind, parallel hotplug/unplug will + * get into the way of this "I hope it works" logic. + */ + + if (sd_device_get_syspath(hostdev, &base) < 0) + return NULL; + pos = strrchr(base, '/'); + if (!pos) + return NULL; + + base = strndupa_safe(base, pos - base); + dir = opendir(base); + if (!dir) + return NULL; + + FOREACH_DIRENT_ALL(de, dir, break) { + unsigned i; + + if (de->d_name[0] == '.') + continue; + if (!IN_SET(de->d_type, DT_DIR, DT_LNK)) + continue; + if (!startswith(de->d_name, "host")) + continue; + if (safe_atou_full(&de->d_name[4], 10, &i) < 0) + continue; + /* + * find the smallest number; the host really needs to export its + * own instance number per parent device; relying on the global host + * enumeration and plainly rebasing the numbers sounds unreliable + */ + if (basenum == -1 || (int) i < basenum) + basenum = i; + } + if (basenum == -1) + return hostdev; + host -= basenum; + + path_prepend(path, "scsi-%i:%i:%i:%i", host, bus, target, lun); + return hostdev; +} + +static sd_device *handle_scsi_hyperv(sd_device *parent, char **path, size_t guid_str_len) { + sd_device *hostdev; + sd_device *vmbusdev; + const char *guid_str; + _cleanup_free_ char *lun = NULL; + char guid[39]; + + assert(parent); + assert(path); + assert(guid_str_len < sizeof(guid)); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0) + return NULL; + + if (sd_device_get_parent(hostdev, &vmbusdev) < 0) + return NULL; + + if (sd_device_get_sysattr_value(vmbusdev, "device_id", &guid_str) < 0) + return NULL; + + if (strlen(guid_str) < guid_str_len || guid_str[0] != '{' || guid_str[guid_str_len-1] != '}') + return NULL; + + size_t k = 0; + for (size_t i = 1; i < guid_str_len-1; i++) { + if (guid_str[i] == '-') + continue; + guid[k++] = guid_str[i]; + } + guid[k] = '\0'; + + format_lun_number(parent, &lun); + path_prepend(path, "vmbus-%s-%s", guid, lun); + return parent; +} + +static sd_device *handle_scsi(sd_device *parent, char **path, char **compat_path, bool *supported_parent) { + const char *devtype, *id, *name; + + if (sd_device_get_devtype(parent, &devtype) < 0 || + !streq(devtype, "scsi_device")) + return parent; + + /* firewire */ + if (sd_device_get_sysattr_value(parent, "ieee1394_id", &id) >= 0) { + path_prepend(path, "ieee1394-0x%s", id); + *supported_parent = true; + return skip_subsystem(parent, "scsi"); + } + + /* scsi sysfs does not have a "subsystem" for the transport */ + if (sd_device_get_syspath(parent, &name) < 0) + return NULL; + + if (strstr(name, "/rport-")) { + *supported_parent = true; + return handle_scsi_fibre_channel(parent, path); + } + + if (strstr(name, "/end_device-")) { + *supported_parent = true; + return handle_scsi_sas(parent, path); + } + + if (strstr(name, "/session")) { + *supported_parent = true; + return handle_scsi_iscsi(parent, path); + } + + if (strstr(name, "/ata")) + return handle_scsi_ata(parent, path, compat_path); + + if (strstr(name, "/vmbus_")) + return handle_scsi_hyperv(parent, path, 37); + else if (strstr(name, "/VMBUS")) + return handle_scsi_hyperv(parent, path, 38); + + return handle_scsi_default(parent, path); +} + +static sd_device *handle_cciss(sd_device *parent, char **path) { + const char *str; + unsigned controller, disk; + + if (sd_device_get_sysname(parent, &str) < 0) + return NULL; + if (sscanf(str, "c%ud%u%*s", &controller, &disk) != 2) + return NULL; + + path_prepend(path, "cciss-disk%u", disk); + return skip_subsystem(parent, "cciss"); +} + +static void handle_scsi_tape(sd_device *dev, char **path) { + const char *name; + + /* must be the last device in the syspath */ + if (*path) + return; + + if (sd_device_get_sysname(dev, &name) < 0) + return; + + if (startswith(name, "nst") && strchr("lma", name[3])) + path_prepend(path, "nst%c", name[3]); + else if (startswith(name, "st") && strchr("lma", name[2])) + path_prepend(path, "st%c", name[2]); +} + +static int get_usb_revision(sd_device *dev) { + uint8_t protocol; + const char *s; + int r; + + assert(dev); + + /* Returns usb revision 1, 2, or 3. */ + + r = sd_device_get_sysattr_value(dev, "bDeviceProtocol", &s); + if (r < 0) + return r; + + r = safe_atou8_full(s, 16, &protocol); + if (r < 0) + return r; + + switch (protocol) { + case USB_HUB_PR_HS_NO_TT: /* Full speed hub (USB1) or Hi-speed hub without TT (USB2) */ + + /* See speed_show() in drivers/usb/core/sysfs.c of the kernel. */ + r = sd_device_get_sysattr_value(dev, "speed", &s); + if (r < 0) + return r; + + if (streq(s, "480")) + return 2; + + return 1; + + case USB_HUB_PR_HS_SINGLE_TT: /* Hi-speed hub with single TT */ + case USB_HUB_PR_HS_MULTI_TT: /* Hi-speed hub with multiple TT */ + return 2; + + case USB_HUB_PR_SS: /* Super speed hub */ + return 3; + + default: + return -EPROTONOSUPPORT; + } +} + +static sd_device *handle_usb(sd_device *parent, char **path) { + const char *devtype, *str, *port; + int r; + + if (sd_device_get_devtype(parent, &devtype) < 0) + return parent; + if (!STR_IN_SET(devtype, "usb_interface", "usb_device")) + return parent; + + if (sd_device_get_sysname(parent, &str) < 0) + return parent; + port = strchr(str, '-'); + if (!port) + return parent; + port++; + + parent = skip_subsystem(parent, "usb"); + if (!parent) + return NULL; + + /* USB host number may change across reboots (and probably even without reboot). The part after USB + * host number is determined by device topology and so does not change. Hence, drop the host number + * and always use '0' instead. + * + * xHCI host controllers may register two (or more?) USB root hubs for USB 2.0 and USB 3.0, and the + * sysname, whose host number replaced with 0, of a device under the hubs may conflict with others. + * To avoid the conflict, let's include the USB revision of the root hub to the PATH_ID. + * See issue https://github.com/systemd/systemd/issues/19406 for more details. */ + r = get_usb_revision(parent); + if (r < 0) { + log_device_debug_errno(parent, r, "Failed to get the USB revision number, ignoring: %m"); + path_prepend(path, "usb-0:%s", port); + } else { + assert(r > 0); + path_prepend(path, "usbv%i-0:%s", r, port); + } + + return parent; +} + +static sd_device *handle_bcma(sd_device *parent, char **path) { + const char *sysname; + unsigned core; + + if (sd_device_get_sysname(parent, &sysname) < 0) + return NULL; + if (sscanf(sysname, "bcma%*u:%u", &core) != 1) + return NULL; + + path_prepend(path, "bcma-%u", core); + return parent; +} + +/* Handle devices of AP bus in System z platform. */ +static sd_device *handle_ap(sd_device *parent, char **path) { + const char *type, *func; + + assert(parent); + assert(path); + + if (sd_device_get_sysattr_value(parent, "type", &type) >= 0 && + sd_device_get_sysattr_value(parent, "ap_functions", &func) >= 0) + path_prepend(path, "ap-%s-%s", type, func); + else { + const char *sysname; + + if (sd_device_get_sysname(parent, &sysname) >= 0) + path_prepend(path, "ap-%s", sysname); + } + + return skip_subsystem(parent, "ap"); +} + +static int find_real_nvme_parent(sd_device *dev, sd_device **ret) { + _cleanup_(sd_device_unrefp) sd_device *nvme = NULL; + const char *sysname, *end, *devpath; + int r; + + /* If the device belongs to "nvme-subsystem" (not to be confused with "nvme"), which happens when + * NVMe multipathing is enabled in the kernel (/sys/module/nvme_core/parameters/multipath is Y), + * then the syspath is something like the following: + * /sys/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1 + * Hence, we need to find the 'real parent' in "nvme" subsystem, e.g, + * /sys/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0 */ + + assert(dev); + assert(ret); + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return r; + + /* The sysname format of nvme block device is nvme%d[c%d]n%d[p%d], e.g. nvme0n1p2 or nvme0c1n2. + * (Note, nvme device with 'c' can be ignored, as they are hidden. ) + * The sysname format of nvme subsystem device is nvme%d. + * See nvme_alloc_ns() and nvme_init_ctrl() in drivers/nvme/host/core.c for more details. */ + end = startswith(sysname, "nvme"); + if (!end) + return -ENXIO; + + end += strspn(end, DIGITS); + sysname = strndupa(sysname, end - sysname); + + r = sd_device_new_from_subsystem_sysname(&nvme, "nvme", sysname); + if (r < 0) + return r; + + r = sd_device_get_devpath(nvme, &devpath); + if (r < 0) + return r; + + /* If the 'real parent' is (still) virtual, e.g. for nvmf disks, refuse to set ID_PATH. */ + if (path_startswith(devpath, "/devices/virtual/")) + return -ENXIO; + + *ret = TAKE_PTR(nvme); + return 0; +} + +static void add_id_with_usb_revision(sd_device *dev, bool test, char *path) { + char *p; + int r; + + assert(dev); + assert(path); + + /* When the path contains the USB revision, let's adds ID_PATH_WITH_USB_REVISION property and + * drop the version specifier for later use. */ + + p = strstrafter(path, "-usbv"); + if (!p) + return; + if (!ascii_isdigit(p[0])) + return; + if (p[1] != '-') + return; + + r = udev_builtin_add_property(dev, test, "ID_PATH_WITH_USB_REVISION", path); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to add ID_PATH_WITH_USB_REVISION property, ignoring: %m"); + + /* Drop the USB revision specifier for backward compatibility. */ + memmove(p - 1, p + 1, strlen(p + 1) + 1); +} + +static void add_id_tag(sd_device *dev, bool test, const char *path) { + char tag[UDEV_NAME_SIZE]; + size_t i = 0; + int r; + + /* compose valid udev tag name */ + for (const char *p = path; *p; p++) { + if (ascii_isdigit(*p) || + ascii_isalpha(*p) || + *p == '-') { + tag[i++] = *p; + continue; + } + + /* skip all leading '_' */ + if (i == 0) + continue; + + /* avoid second '_' */ + if (tag[i-1] == '_') + continue; + + tag[i++] = '_'; + } + /* strip trailing '_' */ + while (i > 0 && tag[i-1] == '_') + i--; + tag[i] = '\0'; + + r = udev_builtin_add_property(dev, test, "ID_PATH_TAG", tag); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to add ID_PATH_TAG property, ignoring: %m"); +} + +static int builtin_path_id(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + _cleanup_(sd_device_unrefp) sd_device *dev_other_branch = NULL; + _cleanup_free_ char *path = NULL, *compat_path = NULL; + bool supported_transport = false, supported_parent = false; + const char *subsystem; + int r; + + /* walk up the chain of devices and compose path */ + for (sd_device *parent = dev; parent; ) { + const char *subsys, *sysname; + + if (sd_device_get_subsystem(parent, &subsys) < 0 || + sd_device_get_sysname(parent, &sysname) < 0) { + ; + } else if (streq(subsys, "scsi_tape")) { + handle_scsi_tape(parent, &path); + } else if (streq(subsys, "scsi")) { + parent = handle_scsi(parent, &path, &compat_path, &supported_parent); + supported_transport = true; + } else if (streq(subsys, "cciss")) { + parent = handle_cciss(parent, &path); + supported_transport = true; + } else if (streq(subsys, "usb")) { + parent = handle_usb(parent, &path); + supported_transport = true; + } else if (streq(subsys, "bcma")) { + parent = handle_bcma(parent, &path); + supported_transport = true; + } else if (streq(subsys, "serio")) { + const char *sysnum; + + if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) { + path_prepend(&path, "serio-%s", sysnum); + parent = skip_subsystem(parent, "serio"); + } + } else if (streq(subsys, "pci")) { + path_prepend(&path, "pci-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "pci-%s", sysname); + parent = skip_subsystem(parent, "pci"); + supported_parent = true; + } else if (streq(subsys, "platform")) { + path_prepend(&path, "platform-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "platform-%s", sysname); + parent = skip_subsystem(parent, "platform"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "amba")) { + path_prepend(&path, "amba-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "amba-%s", sysname); + parent = skip_subsystem(parent, "amba"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "acpi")) { + path_prepend(&path, "acpi-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "acpi-%s", sysname); + parent = skip_subsystem(parent, "acpi"); + supported_parent = true; + } else if (streq(subsys, "xen")) { + path_prepend(&path, "xen-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "xen-%s", sysname); + parent = skip_subsystem(parent, "xen"); + supported_parent = true; + } else if (streq(subsys, "virtio")) { + parent = skip_subsystem(parent, "virtio"); + supported_transport = true; + } else if (streq(subsys, "scm")) { + path_prepend(&path, "scm-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "scm-%s", sysname); + parent = skip_subsystem(parent, "scm"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ccw")) { + path_prepend(&path, "ccw-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "ccw-%s", sysname); + parent = skip_subsystem(parent, "ccw"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ccwgroup")) { + path_prepend(&path, "ccwgroup-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "ccwgroup-%s", sysname); + parent = skip_subsystem(parent, "ccwgroup"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ap")) { + parent = handle_ap(parent, &path); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "iucv")) { + path_prepend(&path, "iucv-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "iucv-%s", sysname); + parent = skip_subsystem(parent, "iucv"); + supported_transport = true; + supported_parent = true; + } else if (STR_IN_SET(subsys, "nvme", "nvme-subsystem")) { + const char *nsid; + + if (sd_device_get_sysattr_value(dev, "nsid", &nsid) >= 0) { + path_prepend(&path, "nvme-%s", nsid); + if (compat_path) + path_prepend(&compat_path, "nvme-%s", nsid); + + if (streq(subsys, "nvme-subsystem")) { + r = find_real_nvme_parent(dev, &dev_other_branch); + if (r < 0) + return r; + + parent = dev_other_branch; + } + + parent = skip_subsystem(parent, "nvme"); + supported_parent = true; + supported_transport = true; + } + } else if (streq(subsys, "spi")) { + const char *sysnum; + + if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) { + path_prepend(&path, "cs-%s", sysnum); + parent = skip_subsystem(parent, "spi"); + } + } + + if (!parent) + break; + if (sd_device_get_parent(parent, &parent) < 0) + break; + } + + if (!path) + return -ENOENT; + + /* + * Do not return devices with an unknown parent device type. They + * might produce conflicting IDs if the parent does not provide a + * unique and predictable name. + */ + if (!supported_parent) + return -ENOENT; + + /* + * Do not return block devices without a well-known transport. Some + * devices do not expose their buses and do not provide a unique + * and predictable name that way. + */ + if (sd_device_get_subsystem(dev, &subsystem) >= 0 && + streq(subsystem, "block") && + !supported_transport) + return -ENOENT; + + add_id_with_usb_revision(dev, test, path); + + r = udev_builtin_add_property(dev, test, "ID_PATH", path); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to add ID_PATH property, ignoring: %m"); + + add_id_tag(dev, test, path); + + /* + * Compatible link generation for ATA devices + * we assign compat_link to the env variable + * ID_PATH_ATA_COMPAT + */ + if (compat_path) + udev_builtin_add_property(dev, test, "ID_PATH_ATA_COMPAT", compat_path); + + return 0; +} + +const UdevBuiltin udev_builtin_path_id = { + .name = "path_id", + .cmd = builtin_path_id, + .help = "Compose persistent device path", + .run_once = true, +}; diff --git a/src/udev/udev-builtin-uaccess.c b/src/udev/udev-builtin-uaccess.c new file mode 100644 index 0000000..da42ef5 --- /dev/null +++ b/src/udev/udev-builtin-uaccess.c @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * manage device node user ACL + */ + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> + +#include "sd-login.h" + +#include "device-util.h" +#include "devnode-acl.h" +#include "errno-util.h" +#include "login-util.h" +#include "log.h" +#include "udev-builtin.h" + +static int builtin_uaccess(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + const char *path = NULL, *seat; + bool changed_acl = false; + uid_t uid; + int r; + + umask(0022); + + /* don't muck around with ACLs when the system is not running systemd */ + if (!logind_running()) + return 0; + + r = sd_device_get_devname(dev, &path); + if (r < 0) { + log_device_error_errno(dev, r, "Failed to get device name: %m"); + goto finish; + } + + if (sd_device_get_property_value(dev, "ID_SEAT", &seat) < 0) + seat = "seat0"; + + r = sd_seat_get_active(seat, NULL, &uid); + if (r < 0) { + if (IN_SET(r, -ENXIO, -ENODATA)) + /* No active session on this seat */ + r = 0; + else + log_device_error_errno(dev, r, "Failed to determine active user on seat %s: %m", seat); + + goto finish; + } + + r = devnode_acl(path, true, false, 0, true, uid); + if (r < 0) { + log_device_full_errno(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to apply ACL: %m"); + goto finish; + } + + changed_acl = true; + r = 0; + +finish: + if (path && !changed_acl) { + int k; + + /* Better be safe than sorry and reset ACL */ + k = devnode_acl(path, true, false, 0, false, 0); + if (k < 0) { + log_device_full_errno(dev, k == -ENOENT ? LOG_DEBUG : LOG_ERR, k, "Failed to apply ACL: %m"); + RET_GATHER(r, k); + } + } + + return r; +} + +const UdevBuiltin udev_builtin_uaccess = { + .name = "uaccess", + .cmd = builtin_uaccess, + .help = "Manage device node user ACL", +}; diff --git a/src/udev/udev-builtin-usb_id.c b/src/udev/udev-builtin-usb_id.c new file mode 100644 index 0000000..8e83c9c --- /dev/null +++ b/src/udev/udev-builtin-usb_id.c @@ -0,0 +1,489 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * USB device properties and persistent device path + * + * Copyright (c) 2005 SUSE Linux Products GmbH, Germany + * Author: Hannes Reinecke <hare@suse.de> + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "device-nodes.h" +#include "device-util.h" +#include "fd-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" +#include "udev-util.h" + +static void set_usb_iftype(char *to, int if_class_num, size_t len) { + const char *type = "generic"; + + switch (if_class_num) { + case 1: + type = "audio"; + break; + case 2: /* CDC-Control */ + break; + case 3: + type = "hid"; + break; + case 5: /* Physical */ + break; + case 6: + type = "media"; + break; + case 7: + type = "printer"; + break; + case 8: + type = "storage"; + break; + case 9: + type = "hub"; + break; + case 0x0a: /* CDC-Data */ + break; + case 0x0b: /* Chip/Smart Card */ + break; + case 0x0d: /* Content Security */ + break; + case 0x0e: + type = "video"; + break; + case 0xdc: /* Diagnostic Device */ + break; + case 0xe0: /* Wireless Controller */ + break; + case 0xfe: /* Application-specific */ + break; + case 0xff: /* Vendor-specific */ + break; + default: + break; + } + strncpy(to, type, len); + to[len-1] = '\0'; +} + +static int set_usb_mass_storage_ifsubtype(char *to, const char *from, size_t len) { + int type_num = 0; + const char *type = "generic"; + + if (safe_atoi(from, &type_num) >= 0) { + switch (type_num) { + case 1: /* RBC devices */ + type = "rbc"; + break; + case 2: + type = "atapi"; + break; + case 3: + type = "tape"; + break; + case 4: /* UFI */ + type = "floppy"; + break; + case 6: /* Transparent SPC-2 devices */ + type = "scsi"; + break; + default: + break; + } + } + strscpy(to, len, type); + return type_num; +} + +static void set_scsi_type(char *to, const char *from, size_t len) { + unsigned type_num; + const char *type = "generic"; + + if (safe_atou(from, &type_num) >= 0) { + switch (type_num) { + case 0: + case 0xe: + type = "disk"; + break; + case 1: + type = "tape"; + break; + case 4: + case 7: + case 0xf: + type = "optical"; + break; + case 5: + type = "cd"; + break; + default: + break; + } + } + strscpy(to, len, type); +} + +#define USB_DT_DEVICE 0x01 +#define USB_DT_INTERFACE 0x04 + +static int dev_if_packed_info(sd_device *dev, char *ifs_str, size_t len) { + _cleanup_close_ int fd = -EBADF; + ssize_t size; + unsigned char buf[18 + 65535]; + size_t pos = 0; + unsigned strpos = 0; + const char *filename, *syspath; + int r; + struct usb_interface_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bInterfaceNumber; + uint8_t bAlternateSetting; + uint8_t bNumEndpoints; + uint8_t bInterfaceClass; + uint8_t bInterfaceSubClass; + uint8_t bInterfaceProtocol; + uint8_t iInterface; + } _packed_; + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + filename = strjoina(syspath, "/descriptors"); + fd = open(filename, O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (fd < 0) + return log_device_debug_errno(dev, errno, "Failed to open \"%s\": %m", filename); + + size = read(fd, buf, sizeof(buf)); + if (size < 18) + return log_device_warning_errno(dev, SYNTHETIC_ERRNO(EIO), + "Short read from \"%s\"", filename); + assert((size_t) size <= sizeof buf); + + ifs_str[0] = '\0'; + while (pos + sizeof(struct usb_interface_descriptor) < (size_t) size && + strpos + 7 < len - 2) { + + struct usb_interface_descriptor *desc; + char if_str[8]; + + desc = (struct usb_interface_descriptor *) (buf + pos); + if (desc->bLength < 3) + break; + if (desc->bLength > size - sizeof(struct usb_interface_descriptor)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EIO), + "Corrupt data read from \"%s\"", filename); + pos += desc->bLength; + + if (desc->bDescriptorType != USB_DT_INTERFACE) + continue; + + if (snprintf(if_str, 8, ":%02x%02x%02x", + desc->bInterfaceClass, + desc->bInterfaceSubClass, + desc->bInterfaceProtocol) != 7) + continue; + + if (strstr(ifs_str, if_str)) + continue; + + memcpy(&ifs_str[strpos], if_str, 8), + strpos += 7; + } + + if (strpos > 0) { + ifs_str[strpos++] = ':'; + ifs_str[strpos++] = '\0'; + } + + return 0; +} + +/* + * A unique USB identification is generated like this: + * + * 1.) Get the USB device type from InterfaceClass and InterfaceSubClass + * 2.) If the device type is 'Mass-Storage/SPC-2' or 'Mass-Storage/RBC', + * use the SCSI vendor and model as USB-Vendor and USB-model. + * 3.) Otherwise, use the USB manufacturer and product as + * USB-Vendor and USB-model. Any non-printable characters + * in those strings will be skipped; a slash '/' will be converted + * into a full stop '.'. + * 4.) If that fails, too, we will use idVendor and idProduct + * as USB-Vendor and USB-model. + * 5.) The USB identification is the USB-vendor and USB-model + * string concatenated with an underscore '_'. + * 6.) If the device supplies a serial number, this number + * is concatenated with the identification with an underscore '_'. + */ +static int builtin_usb_id(UdevEvent *event, int argc, char *argv[], bool test) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + char vendor_str[64] = ""; + char vendor_str_enc[256]; + const char *vendor_id; + char model_str[64] = ""; + char model_str_enc[256]; + const char *product_id; + char serial_str[UDEV_NAME_SIZE] = ""; + char packed_if_str[UDEV_NAME_SIZE] = ""; + char revision_str[64] = ""; + char type_str[64] = ""; + char instance_str[64] = ""; + const char *ifnum = NULL; + const char *driver = NULL; + char serial[256]; + + sd_device *dev_interface, *dev_usb; + const char *if_class, *if_subclass; + unsigned if_class_num; + int protocol = 0; + size_t l; + char *s; + + const char *syspath, *sysname, *devtype, *interface_syspath; + int r; + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return r; + + /* shortcut, if we are called directly for a "usb_device" type */ + if (sd_device_get_devtype(dev, &devtype) >= 0 && streq(devtype, "usb_device")) { + dev_if_packed_info(dev, packed_if_str, sizeof(packed_if_str)); + dev_usb = dev; + goto fallback; + } + + /* usb interface directory */ + r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &dev_interface); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to access usb_interface: %m"); + + r = sd_device_get_syspath(dev_interface, &interface_syspath); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to get syspath: %m"); + (void) sd_device_get_sysattr_value(dev_interface, "bInterfaceNumber", &ifnum); + (void) sd_device_get_sysattr_value(dev_interface, "driver", &driver); + + r = sd_device_get_sysattr_value(dev_interface, "bInterfaceClass", &if_class); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to get bInterfaceClass attribute: %m"); + + r = safe_atou_full(if_class, 16, &if_class_num); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to parse if_class: %m"); + if (if_class_num == 8) { + /* mass storage */ + if (sd_device_get_sysattr_value(dev_interface, "bInterfaceSubClass", &if_subclass) >= 0) + protocol = set_usb_mass_storage_ifsubtype(type_str, if_subclass, sizeof(type_str)-1); + } else + set_usb_iftype(type_str, if_class_num, sizeof(type_str)-1); + + log_device_debug(dev_interface, "if_class:%u protocol:%i", if_class_num, protocol); + + /* usb device directory */ + r = sd_device_get_parent_with_subsystem_devtype(dev_interface, "usb", "usb_device", &dev_usb); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to find parent 'usb' device"); + + /* all interfaces of the device in a single string */ + dev_if_packed_info(dev_usb, packed_if_str, sizeof(packed_if_str)); + + /* mass storage : SCSI or ATAPI */ + if (IN_SET(protocol, 6, 2)) { + sd_device *dev_scsi; + const char *scsi_sysname, *scsi_model, *scsi_vendor, *scsi_type, *scsi_rev; + int host, bus, target, lun; + + /* get scsi device */ + r = sd_device_get_parent_with_subsystem_devtype(dev, "scsi", "scsi_device", &dev_scsi); + if (r < 0) { + log_device_debug_errno(dev, r, "Unable to find parent SCSI device"); + goto fallback; + } + if (sd_device_get_sysname(dev_scsi, &scsi_sysname) < 0) + goto fallback; + if (sscanf(scsi_sysname, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4) { + log_device_debug(dev_scsi, "Invalid SCSI device"); + goto fallback; + } + + /* Generic SPC-2 device */ + r = sd_device_get_sysattr_value(dev_scsi, "vendor", &scsi_vendor); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI vendor attribute: %m"); + goto fallback; + } + encode_devnode_name(scsi_vendor, vendor_str_enc, sizeof(vendor_str_enc)); + udev_replace_whitespace(scsi_vendor, vendor_str, sizeof(vendor_str)-1); + udev_replace_chars(vendor_str, NULL); + + r = sd_device_get_sysattr_value(dev_scsi, "model", &scsi_model); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI model attribute: %m"); + goto fallback; + } + encode_devnode_name(scsi_model, model_str_enc, sizeof(model_str_enc)); + udev_replace_whitespace(scsi_model, model_str, sizeof(model_str)-1); + udev_replace_chars(model_str, NULL); + + r = sd_device_get_sysattr_value(dev_scsi, "type", &scsi_type); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI type attribute: %m"); + goto fallback; + } + set_scsi_type(type_str, scsi_type, sizeof(type_str)-1); + + r = sd_device_get_sysattr_value(dev_scsi, "rev", &scsi_rev); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI revision attribute: %m"); + goto fallback; + } + udev_replace_whitespace(scsi_rev, revision_str, sizeof(revision_str)-1); + udev_replace_chars(revision_str, NULL); + + /* + * some broken devices have the same identifiers + * for all luns, export the target:lun number + */ + sprintf(instance_str, "%d:%d", target, lun); + } + +fallback: + r = sd_device_get_sysattr_value(dev_usb, "idVendor", &vendor_id); + if (r < 0) + return log_device_debug_errno(dev_usb, r, "Failed to get idVendor attribute: %m"); + + r = sd_device_get_sysattr_value(dev_usb, "idProduct", &product_id); + if (r < 0) + return log_device_debug_errno(dev_usb, r, "Failed to get idProduct attribute: %m"); + + /* fall back to USB vendor & device */ + if (vendor_str[0] == '\0') { + const char *usb_vendor; + + if (sd_device_get_sysattr_value(dev_usb, "manufacturer", &usb_vendor) < 0) + usb_vendor = vendor_id; + encode_devnode_name(usb_vendor, vendor_str_enc, sizeof(vendor_str_enc)); + udev_replace_whitespace(usb_vendor, vendor_str, sizeof(vendor_str)-1); + udev_replace_chars(vendor_str, NULL); + } + + if (model_str[0] == '\0') { + const char *usb_model; + + if (sd_device_get_sysattr_value(dev_usb, "product", &usb_model) < 0) + usb_model = product_id; + encode_devnode_name(usb_model, model_str_enc, sizeof(model_str_enc)); + udev_replace_whitespace(usb_model, model_str, sizeof(model_str)-1); + udev_replace_chars(model_str, NULL); + } + + if (revision_str[0] == '\0') { + const char *usb_rev; + + if (sd_device_get_sysattr_value(dev_usb, "bcdDevice", &usb_rev) >= 0) { + udev_replace_whitespace(usb_rev, revision_str, sizeof(revision_str)-1); + udev_replace_chars(revision_str, NULL); + } + } + + if (serial_str[0] == '\0') { + const char *usb_serial; + + if (sd_device_get_sysattr_value(dev_usb, "serial", &usb_serial) >= 0) { + /* http://msdn.microsoft.com/en-us/library/windows/hardware/gg487321.aspx */ + for (const unsigned char *p = (unsigned char*) usb_serial; *p != '\0'; p++) + if (*p < 0x20 || *p > 0x7f || *p == ',') { + usb_serial = NULL; + break; + } + + if (usb_serial) { + udev_replace_whitespace(usb_serial, serial_str, sizeof(serial_str)-1); + udev_replace_chars(serial_str, NULL); + } + } + } + + s = serial; + l = strpcpyl(&s, sizeof(serial), vendor_str, "_", model_str, NULL); + if (!isempty(serial_str)) + l = strpcpyl(&s, l, "_", serial_str, NULL); + + if (!isempty(instance_str)) + strpcpyl(&s, l, "-", instance_str, NULL); + + if (sd_device_get_property_value(dev, "ID_BUS", NULL) >= 0) + log_device_debug(dev, "ID_BUS property is already set, setting only properties prefixed with \"ID_USB_\"."); + else { + udev_builtin_add_property(dev, test, "ID_BUS", "usb"); + + udev_builtin_add_property(dev, test, "ID_MODEL", model_str); + udev_builtin_add_property(dev, test, "ID_MODEL_ENC", model_str_enc); + udev_builtin_add_property(dev, test, "ID_MODEL_ID", product_id); + + udev_builtin_add_property(dev, test, "ID_SERIAL", serial); + if (!isempty(serial_str)) + udev_builtin_add_property(dev, test, "ID_SERIAL_SHORT", serial_str); + + udev_builtin_add_property(dev, test, "ID_VENDOR", vendor_str); + udev_builtin_add_property(dev, test, "ID_VENDOR_ENC", vendor_str_enc); + udev_builtin_add_property(dev, test, "ID_VENDOR_ID", vendor_id); + + udev_builtin_add_property(dev, test, "ID_REVISION", revision_str); + + if (!isempty(type_str)) + udev_builtin_add_property(dev, test, "ID_TYPE", type_str); + + if (!isempty(instance_str)) + udev_builtin_add_property(dev, test, "ID_INSTANCE", instance_str); + } + + /* Also export the same values in the above by prefixing ID_USB_. */ + udev_builtin_add_property(dev, test, "ID_USB_MODEL", model_str); + udev_builtin_add_property(dev, test, "ID_USB_MODEL_ENC", model_str_enc); + udev_builtin_add_property(dev, test, "ID_USB_MODEL_ID", product_id); + udev_builtin_add_property(dev, test, "ID_USB_SERIAL", serial); + if (!isempty(serial_str)) + udev_builtin_add_property(dev, test, "ID_USB_SERIAL_SHORT", serial_str); + + udev_builtin_add_property(dev, test, "ID_USB_VENDOR", vendor_str); + udev_builtin_add_property(dev, test, "ID_USB_VENDOR_ENC", vendor_str_enc); + udev_builtin_add_property(dev, test, "ID_USB_VENDOR_ID", vendor_id); + + udev_builtin_add_property(dev, test, "ID_USB_REVISION", revision_str); + + if (!isempty(type_str)) + udev_builtin_add_property(dev, test, "ID_USB_TYPE", type_str); + + if (!isempty(instance_str)) + udev_builtin_add_property(dev, test, "ID_USB_INSTANCE", instance_str); + + if (!isempty(packed_if_str)) + udev_builtin_add_property(dev, test, "ID_USB_INTERFACES", packed_if_str); + if (ifnum) + udev_builtin_add_property(dev, test, "ID_USB_INTERFACE_NUM", ifnum); + if (driver) + udev_builtin_add_property(dev, test, "ID_USB_DRIVER", driver); + return 0; +} + +const UdevBuiltin udev_builtin_usb_id = { + .name = "usb_id", + .cmd = builtin_usb_id, + .help = "USB device properties", + .run_once = true, +}; diff --git a/src/udev/udev-builtin.c b/src/udev/udev-builtin.c new file mode 100644 index 0000000..bcc2018 --- /dev/null +++ b/src/udev/udev-builtin.c @@ -0,0 +1,156 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <stdio.h> + +#include "device-private.h" +#include "device-util.h" +#include "string-util.h" +#include "strv.h" +#include "udev-builtin.h" + +static bool initialized; + +static const UdevBuiltin *const builtins[_UDEV_BUILTIN_MAX] = { +#if HAVE_BLKID + [UDEV_BUILTIN_BLKID] = &udev_builtin_blkid, +#endif + [UDEV_BUILTIN_BTRFS] = &udev_builtin_btrfs, + [UDEV_BUILTIN_HWDB] = &udev_builtin_hwdb, + [UDEV_BUILTIN_INPUT_ID] = &udev_builtin_input_id, + [UDEV_BUILTIN_KEYBOARD] = &udev_builtin_keyboard, +#if HAVE_KMOD + [UDEV_BUILTIN_KMOD] = &udev_builtin_kmod, +#endif + [UDEV_BUILTIN_NET_DRIVER] = &udev_builtin_net_driver, + [UDEV_BUILTIN_NET_ID] = &udev_builtin_net_id, + [UDEV_BUILTIN_NET_LINK] = &udev_builtin_net_setup_link, + [UDEV_BUILTIN_PATH_ID] = &udev_builtin_path_id, + [UDEV_BUILTIN_USB_ID] = &udev_builtin_usb_id, +#if HAVE_ACL + [UDEV_BUILTIN_UACCESS] = &udev_builtin_uaccess, +#endif +}; + +void udev_builtin_init(void) { + if (initialized) + return; + + for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && builtins[i]->init) + builtins[i]->init(); + + initialized = true; +} + +void udev_builtin_exit(void) { + if (!initialized) + return; + + for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && builtins[i]->exit) + builtins[i]->exit(); + + initialized = false; +} + +bool udev_builtin_should_reload(void) { + for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && builtins[i]->should_reload && builtins[i]->should_reload()) + return true; + return false; +} + +void udev_builtin_list(void) { + for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i]) + fprintf(stderr, " %-14s %s\n", builtins[i]->name, builtins[i]->help); +} + +const char *udev_builtin_name(UdevBuiltinCommand cmd) { + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + + if (!builtins[cmd]) + return NULL; + + return builtins[cmd]->name; +} + +bool udev_builtin_run_once(UdevBuiltinCommand cmd) { + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + + if (!builtins[cmd]) + return false; + + return builtins[cmd]->run_once; +} + +UdevBuiltinCommand udev_builtin_lookup(const char *command) { + size_t n; + + assert(command); + + command += strspn(command, WHITESPACE); + n = strcspn(command, WHITESPACE); + for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && strneq(builtins[i]->name, command, n)) + return i; + + return _UDEV_BUILTIN_INVALID; +} + +int udev_builtin_run(UdevEvent *event, UdevBuiltinCommand cmd, const char *command, bool test) { + _cleanup_strv_free_ char **argv = NULL; + int r; + + assert(event); + assert(event->dev); + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + assert(command); + + if (!builtins[cmd]) + return -EOPNOTSUPP; + + r = strv_split_full(&argv, command, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return r; + + /* we need '0' here to reset the internal state */ + optind = 0; + return builtins[cmd]->cmd(event, strv_length(argv), argv, test); +} + +int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val) { + int r; + + assert(dev); + assert(key); + + r = device_add_property(dev, key, val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to add property '%s%s%s'", + key, val ? "=" : "", strempty(val)); + + if (test) + printf("%s=%s\n", key, strempty(val)); + + return 0; +} + +int udev_builtin_add_propertyf(sd_device *dev, bool test, const char *key, const char *valf, ...) { + _cleanup_free_ char *val = NULL; + va_list ap; + int r; + + assert(dev); + assert(key); + assert(valf); + + va_start(ap, valf); + r = vasprintf(&val, valf, ap); + va_end(ap); + if (r < 0) + return log_oom_debug(); + + return udev_builtin_add_property(dev, test, key, val); +} diff --git a/src/udev/udev-builtin.h b/src/udev/udev-builtin.h new file mode 100644 index 0000000..fcd41d6 --- /dev/null +++ b/src/udev/udev-builtin.h @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "macro.h" +#include "udev-event.h" + +typedef enum UdevBuiltinCommand { +#if HAVE_BLKID + UDEV_BUILTIN_BLKID, +#endif + UDEV_BUILTIN_BTRFS, + UDEV_BUILTIN_HWDB, + UDEV_BUILTIN_INPUT_ID, + UDEV_BUILTIN_KEYBOARD, +#if HAVE_KMOD + UDEV_BUILTIN_KMOD, +#endif + UDEV_BUILTIN_NET_DRIVER, + UDEV_BUILTIN_NET_ID, + UDEV_BUILTIN_NET_LINK, + UDEV_BUILTIN_PATH_ID, + UDEV_BUILTIN_USB_ID, +#if HAVE_ACL + UDEV_BUILTIN_UACCESS, +#endif + _UDEV_BUILTIN_MAX, + _UDEV_BUILTIN_INVALID = -EINVAL, +} UdevBuiltinCommand; + +typedef struct UdevBuiltin { + const char *name; + int (*cmd)(UdevEvent *event, int argc, char *argv[], bool test); + const char *help; + int (*init)(void); + void (*exit)(void); + bool (*should_reload)(void); + bool run_once; +} UdevBuiltin; + +#define UDEV_BUILTIN_CMD_TO_PTR(u) \ + ({ \ + UdevBuiltinCommand _u = (u); \ + _u < 0 ? NULL : (void*)(intptr_t) (_u + 1); \ + }) + +#define PTR_TO_UDEV_BUILTIN_CMD(p) \ + ({ \ + void *_p = (p); \ + _p && (intptr_t)(_p) <= _UDEV_BUILTIN_MAX ? \ + (UdevBuiltinCommand)((intptr_t)_p - 1) : _UDEV_BUILTIN_INVALID; \ + }) + +#if HAVE_BLKID +extern const UdevBuiltin udev_builtin_blkid; +#endif +extern const UdevBuiltin udev_builtin_btrfs; +extern const UdevBuiltin udev_builtin_hwdb; +extern const UdevBuiltin udev_builtin_input_id; +extern const UdevBuiltin udev_builtin_keyboard; +#if HAVE_KMOD +extern const UdevBuiltin udev_builtin_kmod; +#endif +extern const UdevBuiltin udev_builtin_net_driver; +extern const UdevBuiltin udev_builtin_net_id; +extern const UdevBuiltin udev_builtin_net_setup_link; +extern const UdevBuiltin udev_builtin_path_id; +extern const UdevBuiltin udev_builtin_usb_id; +#if HAVE_ACL +extern const UdevBuiltin udev_builtin_uaccess; +#endif + +void udev_builtin_init(void); +void udev_builtin_exit(void); +UdevBuiltinCommand udev_builtin_lookup(const char *command); +const char *udev_builtin_name(UdevBuiltinCommand cmd); +bool udev_builtin_run_once(UdevBuiltinCommand cmd); +int udev_builtin_run(UdevEvent *event, UdevBuiltinCommand cmd, const char *command, bool test); +void udev_builtin_list(void); +bool udev_builtin_should_reload(void); +int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val); +int udev_builtin_add_propertyf(sd_device *dev, bool test, const char *key, const char *valf, ...) _printf_(4, 5); +int udev_builtin_hwdb_lookup(sd_device *dev, const char *prefix, const char *modalias, + const char *filter, bool test); diff --git a/src/udev/udev-ctrl.c b/src/udev/udev-ctrl.c new file mode 100644 index 0000000..2871634 --- /dev/null +++ b/src/udev/udev-ctrl.c @@ -0,0 +1,353 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <poll.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <sys/un.h> +#include <unistd.h> + +#include "sd-event.h" + +#include "alloc-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "iovec-util.h" +#include "socket-util.h" +#include "strxcpyx.h" +#include "udev-ctrl.h" + +/* wire protocol magic must match */ +#define UDEV_CTRL_MAGIC 0xdead1dea + +typedef struct UdevCtrlMessageWire { + char version[16]; + unsigned magic; + UdevCtrlMessageType type; + UdevCtrlMessageValue value; +} UdevCtrlMessageWire; + +struct UdevCtrl { + unsigned n_ref; + int sock; + int sock_connect; + union sockaddr_union saddr; + socklen_t addrlen; + bool bound; + bool connected; + sd_event *event; + sd_event_source *event_source; + sd_event_source *event_source_connect; + udev_ctrl_handler_t callback; + void *userdata; +}; + +int udev_ctrl_new_from_fd(UdevCtrl **ret, int fd) { + _cleanup_close_ int sock = -EBADF; + UdevCtrl *uctrl; + + assert(ret); + + if (fd < 0) { + sock = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_NONBLOCK|SOCK_CLOEXEC, 0); + if (sock < 0) + return log_error_errno(errno, "Failed to create socket: %m"); + } + + uctrl = new(UdevCtrl, 1); + if (!uctrl) + return -ENOMEM; + + *uctrl = (UdevCtrl) { + .n_ref = 1, + .sock = fd >= 0 ? fd : TAKE_FD(sock), + .sock_connect = -EBADF, + .bound = fd >= 0, + }; + + uctrl->saddr.un = (struct sockaddr_un) { + .sun_family = AF_UNIX, + .sun_path = "/run/udev/control", + }; + + uctrl->addrlen = SOCKADDR_UN_LEN(uctrl->saddr.un); + + *ret = TAKE_PTR(uctrl); + return 0; +} + +int udev_ctrl_enable_receiving(UdevCtrl *uctrl) { + assert(uctrl); + + if (uctrl->bound) + return 0; + + (void) sockaddr_un_unlink(&uctrl->saddr.un); + if (bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen) < 0) + return log_error_errno(errno, "Failed to bind udev control socket: %m"); + + if (listen(uctrl->sock, 0) < 0) + return log_error_errno(errno, "Failed to listen udev control socket: %m"); + + uctrl->bound = true; + return 0; +} + +static void udev_ctrl_disconnect(UdevCtrl *uctrl) { + if (!uctrl) + return; + + uctrl->event_source_connect = sd_event_source_unref(uctrl->event_source_connect); + uctrl->sock_connect = safe_close(uctrl->sock_connect); +} + +static UdevCtrl *udev_ctrl_free(UdevCtrl *uctrl) { + assert(uctrl); + + udev_ctrl_disconnect(uctrl); + + sd_event_source_unref(uctrl->event_source); + safe_close(uctrl->sock); + + sd_event_unref(uctrl->event); + return mfree(uctrl); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(UdevCtrl, udev_ctrl, udev_ctrl_free); + +int udev_ctrl_attach_event(UdevCtrl *uctrl, sd_event *event) { + int r; + + assert_return(uctrl, -EINVAL); + assert_return(!uctrl->event, -EBUSY); + + if (event) + uctrl->event = sd_event_ref(event); + else { + r = sd_event_default(&uctrl->event); + if (r < 0) + return r; + } + + return 0; +} + +sd_event_source *udev_ctrl_get_event_source(UdevCtrl *uctrl) { + assert(uctrl); + + return uctrl->event_source; +} + +static void udev_ctrl_disconnect_and_listen_again(UdevCtrl *uctrl) { + udev_ctrl_disconnect(uctrl); + udev_ctrl_unref(uctrl); + (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_ON); + /* We don't return NULL here because uctrl is not freed */ +} + +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UdevCtrl*, udev_ctrl_disconnect_and_listen_again, NULL); + +static int udev_ctrl_connection_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(udev_ctrl_disconnect_and_listen_againp) UdevCtrl *uctrl = NULL; + UdevCtrlMessageWire msg_wire; + struct iovec iov = IOVEC_MAKE(&msg_wire, sizeof(UdevCtrlMessageWire)); + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; + struct msghdr smsg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct ucred *cred; + ssize_t size; + + assert(userdata); + + /* When UDEV_CTRL_EXIT is received, manager unref udev_ctrl object. + * To avoid the object freed, let's increment the refcount. */ + uctrl = udev_ctrl_ref(userdata); + + size = next_datagram_size_fd(fd); + if (size < 0) + return log_error_errno(size, "Failed to get size of message: %m"); + if (size == 0) + return 0; /* Client disconnects? */ + + size = recvmsg_safe(fd, &smsg, 0); + if (size == -EINTR) + return 0; + if (size < 0) + return log_error_errno(size, "Failed to receive ctrl message: %m"); + + cmsg_close_all(&smsg); + + cred = CMSG_FIND_DATA(&smsg, SOL_SOCKET, SCM_CREDENTIALS, struct ucred); + if (!cred) { + log_error("No sender credentials received, ignoring message"); + return 0; + } + + if (cred->uid != 0) { + log_error("Invalid sender uid "UID_FMT", ignoring message", cred->uid); + return 0; + } + + if (msg_wire.magic != UDEV_CTRL_MAGIC) { + log_error("Message magic 0x%08x doesn't match, ignoring message", msg_wire.magic); + return 0; + } + + if (msg_wire.type == _UDEV_CTRL_END_MESSAGES) + return 0; + + if (uctrl->callback) + (void) uctrl->callback(uctrl, msg_wire.type, &msg_wire.value, uctrl->userdata); + + /* Do not disconnect and wait for next message. */ + uctrl = udev_ctrl_unref(uctrl); + return 0; +} + +static int udev_ctrl_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + UdevCtrl *uctrl = ASSERT_PTR(userdata); + _cleanup_close_ int sock = -EBADF; + struct ucred ucred; + int r; + + sock = accept4(fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK); + if (sock < 0) { + if (ERRNO_IS_ACCEPT_AGAIN(errno)) + return 0; + + return log_error_errno(errno, "Failed to accept ctrl connection: %m"); + } + + /* check peer credential of connection */ + r = getpeercred(sock, &ucred); + if (r < 0) { + log_error_errno(r, "Failed to receive credentials of ctrl connection: %m"); + return 0; + } + + if (ucred.uid > 0) { + log_error("Invalid sender uid "UID_FMT", closing connection", ucred.uid); + return 0; + } + + /* enable receiving of the sender credentials in the messages */ + r = setsockopt_int(sock, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + log_warning_errno(r, "Failed to set SO_PASSCRED, ignoring: %m"); + + r = sd_event_add_io(uctrl->event, &uctrl->event_source_connect, sock, EPOLLIN, udev_ctrl_connection_event_handler, uctrl); + if (r < 0) { + log_error_errno(r, "Failed to create event source for udev control connection: %m"); + return 0; + } + + (void) sd_event_source_set_description(uctrl->event_source_connect, "udev-ctrl-connection"); + + /* Do not accept multiple connection. */ + (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_OFF); + + uctrl->sock_connect = TAKE_FD(sock); + return 0; +} + +int udev_ctrl_start(UdevCtrl *uctrl, udev_ctrl_handler_t callback, void *userdata) { + int r; + + assert(uctrl); + + if (!uctrl->event) { + r = udev_ctrl_attach_event(uctrl, NULL); + if (r < 0) + return r; + } + + r = udev_ctrl_enable_receiving(uctrl); + if (r < 0) + return r; + + uctrl->callback = callback; + uctrl->userdata = userdata; + + r = sd_event_add_io(uctrl->event, &uctrl->event_source, uctrl->sock, EPOLLIN, udev_ctrl_event_handler, uctrl); + if (r < 0) + return r; + + (void) sd_event_source_set_description(uctrl->event_source, "udev-ctrl"); + + return 0; +} + +int udev_ctrl_send(UdevCtrl *uctrl, UdevCtrlMessageType type, const void *data) { + UdevCtrlMessageWire ctrl_msg_wire = { + .version = "udev-" STRINGIFY(PROJECT_VERSION), + .magic = UDEV_CTRL_MAGIC, + .type = type, + }; + + if (type == UDEV_CTRL_SET_ENV) { + assert(data); + strscpy(ctrl_msg_wire.value.buf, sizeof(ctrl_msg_wire.value.buf), data); + } else if (IN_SET(type, UDEV_CTRL_SET_LOG_LEVEL, UDEV_CTRL_SET_CHILDREN_MAX)) + ctrl_msg_wire.value.intval = PTR_TO_INT(data); + + if (!uctrl->connected) { + if (connect(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen) < 0) + return -errno; + uctrl->connected = true; + } + + if (send(uctrl->sock, &ctrl_msg_wire, sizeof(ctrl_msg_wire), 0) < 0) + return -errno; + + return 0; +} + +int udev_ctrl_wait(UdevCtrl *uctrl, usec_t timeout) { + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *source_io = NULL, *source_timeout = NULL; + int r; + + assert(uctrl); + + if (uctrl->sock < 0) + return 0; + if (!uctrl->connected) + return 0; + + r = udev_ctrl_send(uctrl, _UDEV_CTRL_END_MESSAGES, NULL); + if (r < 0) + return r; + + if (timeout == 0) + return 0; + + if (!uctrl->event) { + r = udev_ctrl_attach_event(uctrl, NULL); + if (r < 0) + return r; + } + + r = sd_event_add_io(uctrl->event, &source_io, uctrl->sock, EPOLLIN, NULL, INT_TO_PTR(0)); + if (r < 0) + return r; + + (void) sd_event_source_set_description(source_io, "udev-ctrl-wait-io"); + + if (timeout != USEC_INFINITY) { + r = sd_event_add_time_relative( + uctrl->event, &source_timeout, CLOCK_BOOTTIME, + timeout, + 0, NULL, INT_TO_PTR(-ETIMEDOUT)); + if (r < 0) + return r; + + (void) sd_event_source_set_description(source_timeout, "udev-ctrl-wait-timeout"); + } + + return sd_event_loop(uctrl->event); +} diff --git a/src/udev/udev-ctrl.h b/src/udev/udev-ctrl.h new file mode 100644 index 0000000..11fc0b6 --- /dev/null +++ b/src/udev/udev-ctrl.h @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "sd-event.h" + +#include "macro.h" +#include "time-util.h" + +typedef struct UdevCtrl UdevCtrl; + +typedef enum UdevCtrlMessageType { + _UDEV_CTRL_END_MESSAGES, + UDEV_CTRL_SET_LOG_LEVEL, + UDEV_CTRL_STOP_EXEC_QUEUE, + UDEV_CTRL_START_EXEC_QUEUE, + UDEV_CTRL_RELOAD, + UDEV_CTRL_SET_ENV, + UDEV_CTRL_SET_CHILDREN_MAX, + UDEV_CTRL_PING, + UDEV_CTRL_EXIT, +} UdevCtrlMessageType; + +typedef union UdevCtrlMessageValue { + int intval; + char buf[256]; +} UdevCtrlMessageValue; + +typedef int (*udev_ctrl_handler_t)(UdevCtrl *udev_ctrl, UdevCtrlMessageType type, + const UdevCtrlMessageValue *value, void *userdata); + +int udev_ctrl_new_from_fd(UdevCtrl **ret, int fd); +static inline int udev_ctrl_new(UdevCtrl **ret) { + return udev_ctrl_new_from_fd(ret, -1); +} + +int udev_ctrl_enable_receiving(UdevCtrl *uctrl); +UdevCtrl *udev_ctrl_ref(UdevCtrl *uctrl); +UdevCtrl *udev_ctrl_unref(UdevCtrl *uctrl); +int udev_ctrl_attach_event(UdevCtrl *uctrl, sd_event *event); +int udev_ctrl_start(UdevCtrl *uctrl, udev_ctrl_handler_t callback, void *userdata); +sd_event_source *udev_ctrl_get_event_source(UdevCtrl *uctrl); + +int udev_ctrl_wait(UdevCtrl *uctrl, usec_t timeout); + +int udev_ctrl_send(UdevCtrl *uctrl, UdevCtrlMessageType type, const void *data); +static inline int udev_ctrl_send_set_log_level(UdevCtrl *uctrl, int priority) { + return udev_ctrl_send(uctrl, UDEV_CTRL_SET_LOG_LEVEL, INT_TO_PTR(priority)); +} + +static inline int udev_ctrl_send_stop_exec_queue(UdevCtrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_STOP_EXEC_QUEUE, NULL); +} + +static inline int udev_ctrl_send_start_exec_queue(UdevCtrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_START_EXEC_QUEUE, NULL); +} + +static inline int udev_ctrl_send_reload(UdevCtrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_RELOAD, NULL); +} + +static inline int udev_ctrl_send_set_env(UdevCtrl *uctrl, const char *key) { + return udev_ctrl_send(uctrl, UDEV_CTRL_SET_ENV, key); +} + +static inline int udev_ctrl_send_set_children_max(UdevCtrl *uctrl, int count) { + return udev_ctrl_send(uctrl, UDEV_CTRL_SET_CHILDREN_MAX, INT_TO_PTR(count)); +} + +static inline int udev_ctrl_send_ping(UdevCtrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_PING, NULL); +} + +static inline int udev_ctrl_send_exit(UdevCtrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_EXIT, NULL); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevCtrl*, udev_ctrl_unref); diff --git a/src/udev/udev-event.c b/src/udev/udev-event.c new file mode 100644 index 0000000..ed22c8b --- /dev/null +++ b/src/udev/udev-event.c @@ -0,0 +1,411 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "alloc-util.h" +#include "device-internal.h" +#include "device-private.h" +#include "device-util.h" +#include "fs-util.h" +#include "netif-naming-scheme.h" +#include "netlink-util.h" +#include "path-util.h" +#include "string-util.h" +#include "strv.h" +#include "udev-event.h" +#include "udev-node.h" +#include "udev-trace.h" +#include "udev-util.h" +#include "udev-watch.h" +#include "user-util.h" + +UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl, int log_level) { + UdevEvent *event; + + assert(dev); + + event = new(UdevEvent, 1); + if (!event) + return NULL; + + *event = (UdevEvent) { + .dev = sd_device_ref(dev), + .birth_usec = now(CLOCK_MONOTONIC), + .exec_delay_usec = exec_delay_usec, + .rtnl = sd_netlink_ref(rtnl), + .uid = UID_INVALID, + .gid = GID_INVALID, + .mode = MODE_INVALID, + .log_level_was_debug = log_level == LOG_DEBUG, + .default_log_level = log_level, + }; + + return event; +} + +UdevEvent *udev_event_free(UdevEvent *event) { + if (!event) + return NULL; + + sd_device_unref(event->dev); + sd_device_unref(event->dev_db_clone); + sd_netlink_unref(event->rtnl); + ordered_hashmap_free_free_key(event->run_list); + ordered_hashmap_free_free_free(event->seclabel_list); + free(event->program_result); + free(event->name); + strv_free(event->altnames); + + return mfree(event); +} + +static int device_rename(sd_device *device, const char *name) { + _cleanup_free_ char *new_syspath = NULL; + const char *s; + int r; + + assert(device); + assert(name); + + if (!filename_is_valid(name)) + return -EINVAL; + + r = sd_device_get_syspath(device, &s); + if (r < 0) + return r; + + r = path_extract_directory(s, &new_syspath); + if (r < 0) + return r; + + if (!path_extend(&new_syspath, name)) + return -ENOMEM; + + if (!path_is_safe(new_syspath)) + return -EINVAL; + + /* At the time this is called, the renamed device may not exist yet. Hence, we cannot validate + * the new syspath. */ + r = device_set_syspath(device, new_syspath, /* verify = */ false); + if (r < 0) + return r; + + r = sd_device_get_property_value(device, "INTERFACE", &s); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + /* like DEVPATH_OLD, INTERFACE_OLD is not saved to the db, but only stays around for the current event */ + r = device_add_property_internal(device, "INTERFACE_OLD", s); + if (r < 0) + return r; + + return device_add_property_internal(device, "INTERFACE", name); +} + +static int rename_netif(UdevEvent *event) { + _cleanup_free_ char *old_syspath = NULL, *old_sysname = NULL; + const char *s; + sd_device *dev; + int ifindex, r; + + assert(event); + + if (!event->name) + return 0; /* No new name is requested. */ + + dev = ASSERT_PTR(event->dev); + + r = sd_device_get_ifindex(dev, &ifindex); + if (r == -ENOENT) + return 0; /* Device is not a network interface. */ + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get ifindex: %m"); + + if (naming_scheme_has(NAMING_REPLACE_STRICTLY) && + !ifname_valid(event->name)) { + log_device_warning(dev, "Invalid network interface name, ignoring: %s", event->name); + return 0; + } + + r = sd_device_get_sysname(dev, &s); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get sysname: %m"); + + if (streq(event->name, s)) + return 0; /* The interface name is already requested name. */ + + old_sysname = strdup(s); + if (!old_sysname) + return -ENOMEM; + + r = sd_device_get_syspath(dev, &s); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get syspath: %m"); + + old_syspath = strdup(s); + if (!old_syspath) + return -ENOMEM; + + r = device_rename(dev, event->name); + if (r < 0) { + /* Here and below, use dev_db_clone for logging, otherwise, logged message is prefixed with + * the new interface name, and e.g. 'networkctl status INTERFACE' does not show the message. */ + log_device_warning_errno(event->dev_db_clone, r, + "Failed to update properties with new name '%s': %m", event->name); + goto revert; + } + + /* Set ID_RENAMING boolean property here. It will be dropped when the corresponding move uevent is processed. */ + r = device_add_property(dev, "ID_RENAMING", "1"); + if (r < 0) { + log_device_warning_errno(event->dev_db_clone, r, "Failed to add 'ID_RENAMING' property: %m"); + goto revert; + } + + /* Also set ID_RENAMING boolean property to cloned sd_device object and save it to database + * before calling rtnl_set_link_name(). Otherwise, clients (e.g., systemd-networkd) may receive + * RTM_NEWLINK netlink message before the database is updated. */ + r = device_add_property(event->dev_db_clone, "ID_RENAMING", "1"); + if (r < 0) { + log_device_warning_errno(event->dev_db_clone, r, "Failed to add 'ID_RENAMING' property: %m"); + goto revert; + } + + r = device_update_db(event->dev_db_clone); + if (r < 0) { + log_device_debug_errno(event->dev_db_clone, r, "Failed to update database under /run/udev/data/: %m"); + goto revert; + } + + r = rtnl_set_link_name(&event->rtnl, ifindex, event->name, event->altnames); + if (r < 0) { + if (r == -EBUSY) { + log_device_info(event->dev_db_clone, + "Network interface '%s' is already up, cannot rename to '%s'.", + old_sysname, event->name); + r = 0; + } else + log_device_error_errno(event->dev_db_clone, r, + "Failed to rename network interface %i from '%s' to '%s': %m", + ifindex, old_sysname, event->name); + goto revert; + } + + log_device_debug(dev, "Network interface %i is renamed from '%s' to '%s'", ifindex, old_sysname, event->name); + return 1; + +revert: + /* Restore 'dev_db_clone' */ + (void) device_add_property(event->dev_db_clone, "ID_RENAMING", NULL); + (void) device_update_db(event->dev_db_clone); + + /* Restore 'dev' */ + (void) device_set_syspath(dev, old_syspath, /* verify = */ false); + if (sd_device_get_property_value(dev, "INTERFACE_OLD", &s) >= 0) { + (void) device_add_property_internal(dev, "INTERFACE", s); + (void) device_add_property_internal(dev, "INTERFACE_OLD", NULL); + } + (void) device_add_property(dev, "ID_RENAMING", NULL); + + return r; +} + +static int assign_altnames(UdevEvent *event) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + int ifindex, r; + const char *s; + + if (strv_isempty(event->altnames)) + return 0; + + r = sd_device_get_ifindex(dev, &ifindex); + if (r == -ENOENT) + return 0; /* Device is not a network interface. */ + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get ifindex: %m"); + + r = sd_device_get_sysname(dev, &s); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to get sysname: %m"); + + /* Filter out the current interface name. */ + strv_remove(event->altnames, s); + + r = rtnl_append_link_alternative_names(&event->rtnl, ifindex, event->altnames); + if (r < 0) + log_device_full_errno(dev, r == -EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, r, + "Could not set AlternativeName= or apply AlternativeNamesPolicy=, ignoring: %m"); + + return 0; +} + +static int update_devnode(UdevEvent *event) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + int r; + + r = sd_device_get_devnum(dev, NULL); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get devnum: %m"); + + if (!uid_is_valid(event->uid)) { + r = device_get_devnode_uid(dev, &event->uid); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(dev, r, "Failed to get devnode UID: %m"); + } + + if (!gid_is_valid(event->gid)) { + r = device_get_devnode_gid(dev, &event->gid); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(dev, r, "Failed to get devnode GID: %m"); + } + + if (event->mode == MODE_INVALID) { + r = device_get_devnode_mode(dev, &event->mode); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(dev, r, "Failed to get devnode mode: %m"); + } + + bool apply_mac = device_for_action(dev, SD_DEVICE_ADD); + + r = udev_node_apply_permissions(dev, apply_mac, event->mode, event->uid, event->gid, event->seclabel_list); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to apply devnode permissions: %m"); + + return udev_node_update(dev, event->dev_db_clone); +} + +static int event_execute_rules_on_remove( + UdevEvent *event, + int inotify_fd, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRules *rules) { + + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + int r; + + r = device_read_db_internal(dev, true); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to read database under /run/udev/data/: %m"); + + r = device_tag_index(dev, NULL, false); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to remove corresponding tag files under /run/udev/tag/, ignoring: %m"); + + r = device_delete_db(dev); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to delete database under /run/udev/data/, ignoring: %m"); + + r = udev_watch_end(inotify_fd, dev); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to remove inotify watch, ignoring: %m"); + + r = udev_rules_apply_to_event(rules, event, timeout_usec, timeout_signal, properties_list); + + if (sd_device_get_devnum(dev, NULL) >= 0) + (void) udev_node_remove(dev); + + return r; +} + +static int copy_all_tags(sd_device *d, sd_device *s) { + int r; + + assert(d); + + if (!s) + return 0; + + FOREACH_DEVICE_TAG(s, tag) { + r = device_add_tag(d, tag, false); + if (r < 0) + return r; + } + + return 0; +} + +int udev_event_execute_rules( + UdevEvent *event, + int inotify_fd, /* This may be negative */ + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRules *rules) { + + sd_device_action_t action; + sd_device *dev; + int r; + + dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + assert(rules); + + r = sd_device_get_action(dev, &action); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get ACTION: %m"); + + if (action == SD_DEVICE_REMOVE) + return event_execute_rules_on_remove(event, inotify_fd, timeout_usec, timeout_signal, properties_list, rules); + + /* Disable watch during event processing. */ + r = udev_watch_end(inotify_fd, dev); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to remove inotify watch, ignoring: %m"); + + r = device_clone_with_db(dev, &event->dev_db_clone); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to clone sd_device object: %m"); + + r = copy_all_tags(dev, event->dev_db_clone); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to copy all tags from old database entry, ignoring: %m"); + + /* Drop previously added property for safety to make IMPORT{db}="ID_RENAMING" not work. This is + * mostly for 'move' uevent, but let's do unconditionally. Why? If a network interface is renamed in + * initrd, then udevd may lose the 'move' uevent during switching root. Usually, we do not set the + * persistent flag for network interfaces, but user may set it. Just for safety. */ + r = device_add_property(event->dev_db_clone, "ID_RENAMING", NULL); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to remove 'ID_RENAMING' property: %m"); + + DEVICE_TRACE_POINT(rules_start, dev); + + r = udev_rules_apply_to_event(rules, event, timeout_usec, timeout_signal, properties_list); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to apply udev rules: %m"); + + DEVICE_TRACE_POINT(rules_finished, dev); + + if (action == SD_DEVICE_ADD) { + r = rename_netif(event); + if (r < 0) + return r; + if (r == 0) + (void) assign_altnames(event); + } + + r = update_devnode(event); + if (r < 0) + return r; + + /* preserve old, or get new initialization timestamp */ + r = device_ensure_usec_initialized(dev, event->dev_db_clone); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to set initialization timestamp: %m"); + + /* (re)write database file */ + r = device_tag_index(dev, event->dev_db_clone, true); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to update tags under /run/udev/tag/: %m"); + + r = device_update_db(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m"); + + device_set_is_initialized(dev); + + return 0; +} diff --git a/src/udev/udev-event.h b/src/udev/udev-event.h new file mode 100644 index 0000000..6b94fd0 --- /dev/null +++ b/src/udev/udev-event.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * Copyright © 2003 Greg Kroah-Hartman <greg@kroah.com> + */ + +#include <stdbool.h> +#include <stddef.h> + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "hashmap.h" +#include "macro.h" +#include "time-util.h" +#include "udev-rules.h" +#include "user-util.h" + +typedef struct UdevEvent { + sd_device *dev; + sd_device *dev_parent; + sd_device *dev_db_clone; + char *name; + char **altnames; + char *program_result; + mode_t mode; + uid_t uid; + gid_t gid; + OrderedHashmap *seclabel_list; + OrderedHashmap *run_list; + usec_t exec_delay_usec; + usec_t birth_usec; + sd_netlink *rtnl; + unsigned builtin_run; + unsigned builtin_ret; + UdevRuleEscapeType esc:8; + bool inotify_watch; + bool inotify_watch_final; + bool group_final; + bool owner_final; + bool mode_final; + bool name_final; + bool devlink_final; + bool run_final; + bool log_level_was_debug; + int default_log_level; +} UdevEvent; + +UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl, int log_level); +UdevEvent *udev_event_free(UdevEvent *event); +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevEvent*, udev_event_free); + +int udev_event_execute_rules( + UdevEvent *event, + int inotify_fd, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRules *rules); diff --git a/src/udev/udev-format.c b/src/udev/udev-format.c new file mode 100644 index 0000000..05ed9fd --- /dev/null +++ b/src/udev/udev-format.c @@ -0,0 +1,550 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "device-util.h" +#include "errno-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-event.h" +#include "udev-format.h" +#include "udev-util.h" + +typedef enum { + FORMAT_SUBST_DEVNODE, + FORMAT_SUBST_ATTR, + FORMAT_SUBST_ENV, + FORMAT_SUBST_KERNEL, + FORMAT_SUBST_KERNEL_NUMBER, + FORMAT_SUBST_DRIVER, + FORMAT_SUBST_DEVPATH, + FORMAT_SUBST_ID, + FORMAT_SUBST_MAJOR, + FORMAT_SUBST_MINOR, + FORMAT_SUBST_RESULT, + FORMAT_SUBST_PARENT, + FORMAT_SUBST_NAME, + FORMAT_SUBST_LINKS, + FORMAT_SUBST_ROOT, + FORMAT_SUBST_SYS, + _FORMAT_SUBST_TYPE_MAX, + _FORMAT_SUBST_TYPE_INVALID = -EINVAL, +} FormatSubstitutionType; + +struct subst_map_entry { + const char *name; + const char fmt; + FormatSubstitutionType type; +}; + +static const struct subst_map_entry map[] = { + { .name = "devnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE }, + { .name = "tempnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE }, /* deprecated */ + { .name = "attr", .fmt = 's', .type = FORMAT_SUBST_ATTR }, + { .name = "sysfs", .fmt = 's', .type = FORMAT_SUBST_ATTR }, /* deprecated */ + { .name = "env", .fmt = 'E', .type = FORMAT_SUBST_ENV }, + { .name = "kernel", .fmt = 'k', .type = FORMAT_SUBST_KERNEL }, + { .name = "number", .fmt = 'n', .type = FORMAT_SUBST_KERNEL_NUMBER }, + { .name = "driver", .fmt = 'd', .type = FORMAT_SUBST_DRIVER }, + { .name = "devpath", .fmt = 'p', .type = FORMAT_SUBST_DEVPATH }, + { .name = "id", .fmt = 'b', .type = FORMAT_SUBST_ID }, + { .name = "major", .fmt = 'M', .type = FORMAT_SUBST_MAJOR }, + { .name = "minor", .fmt = 'm', .type = FORMAT_SUBST_MINOR }, + { .name = "result", .fmt = 'c', .type = FORMAT_SUBST_RESULT }, + { .name = "parent", .fmt = 'P', .type = FORMAT_SUBST_PARENT }, + { .name = "name", .fmt = 'D', .type = FORMAT_SUBST_NAME }, + { .name = "links", .fmt = 'L', .type = FORMAT_SUBST_LINKS }, + { .name = "root", .fmt = 'r', .type = FORMAT_SUBST_ROOT }, + { .name = "sys", .fmt = 'S', .type = FORMAT_SUBST_SYS }, +}; + +static const char *format_type_to_string(FormatSubstitutionType t) { + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (map[i].type == t) + return map[i].name; + return NULL; +} + +static char format_type_to_char(FormatSubstitutionType t) { + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (map[i].type == t) + return map[i].fmt; + return '\0'; +} + +static int get_subst_type(const char **str, bool strict, FormatSubstitutionType *ret_type, char ret_attr[static UDEV_PATH_SIZE]) { + const char *p = *str, *q = NULL; + size_t i; + + assert(str); + assert(*str); + assert(ret_type); + assert(ret_attr); + + if (*p == '$') { + p++; + if (*p == '$') { + *str = p; + return 0; + } + for (i = 0; i < ELEMENTSOF(map); i++) + if ((q = startswith(p, map[i].name))) + break; + } else if (*p == '%') { + p++; + if (*p == '%') { + *str = p; + return 0; + } + + for (i = 0; i < ELEMENTSOF(map); i++) + if (*p == map[i].fmt) { + q = p + 1; + break; + } + } else + return 0; + if (!q) + /* When 'strict' flag is set, then '$' and '%' must be escaped. */ + return strict ? -EINVAL : 0; + + if (*q == '{') { + const char *start, *end; + size_t len; + + start = q + 1; + end = strchr(start, '}'); + if (!end) + return -EINVAL; + + len = end - start; + if (len == 0 || len >= UDEV_PATH_SIZE) + return -EINVAL; + + strnscpy(ret_attr, UDEV_PATH_SIZE, start, len); + q = end + 1; + } else + *ret_attr = '\0'; + + *str = q; + *ret_type = map[i].type; + return 1; +} + +static int safe_atou_optional_plus(const char *s, unsigned *ret) { + const char *p; + int r; + + assert(s); + assert(ret); + + /* Returns 1 if plus, 0 if no plus, negative on error */ + + p = endswith(s, "+"); + if (p) + s = strndupa_safe(s, p - s); + + r = safe_atou(s, ret); + if (r < 0) + return r; + + return !!p; +} + +static ssize_t udev_event_subst_format( + UdevEvent *event, + FormatSubstitutionType type, + const char *attr, + char *dest, + size_t l, + Hashmap *global_props, + bool *ret_truncated) { + + sd_device *parent, *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + const char *val = NULL; + bool truncated = false; + char *s = dest; + int r; + + switch (type) { + case FORMAT_SUBST_DEVPATH: + r = sd_device_get_devpath(dev, &val); + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + case FORMAT_SUBST_KERNEL: + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + case FORMAT_SUBST_KERNEL_NUMBER: + r = sd_device_get_sysnum(dev, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + case FORMAT_SUBST_ID: + if (!event->dev_parent) + goto null_terminate; + r = sd_device_get_sysname(event->dev_parent, &val); + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + case FORMAT_SUBST_DRIVER: + if (!event->dev_parent) + goto null_terminate; + r = sd_device_get_driver(event->dev_parent, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + case FORMAT_SUBST_MAJOR: + case FORMAT_SUBST_MINOR: { + dev_t devnum; + + r = sd_device_get_devnum(dev, &devnum); + if (r < 0 && r != -ENOENT) + return r; + strpcpyf_full(&s, l, &truncated, "%u", r < 0 ? 0 : type == FORMAT_SUBST_MAJOR ? major(devnum) : minor(devnum)); + break; + } + case FORMAT_SUBST_RESULT: { + unsigned index = 0; /* 0 means whole string */ + bool has_plus; + + if (!event->program_result) + goto null_terminate; + + if (!isempty(attr)) { + r = safe_atou_optional_plus(attr, &index); + if (r < 0) + return r; + + has_plus = r; + } + + if (index == 0) + strpcpy_full(&s, l, event->program_result, &truncated); + else { + const char *start, *p; + unsigned i; + + p = skip_leading_chars(event->program_result, NULL); + + for (i = 1; i < index; i++) { + while (*p && !strchr(WHITESPACE, *p)) + p++; + p = skip_leading_chars(p, NULL); + if (*p == '\0') + break; + } + if (i != index) { + log_device_debug(dev, "requested part of result string not found"); + goto null_terminate; + } + + start = p; + /* %c{2+} copies the whole string from the second part on */ + if (has_plus) + strpcpy_full(&s, l, start, &truncated); + else { + while (*p && !strchr(WHITESPACE, *p)) + p++; + strnpcpy_full(&s, l, start, p - start, &truncated); + } + } + break; + } + case FORMAT_SUBST_ATTR: { + char vbuf[UDEV_NAME_SIZE]; + int count; + bool t; + + if (isempty(attr)) + return -EINVAL; + + /* try to read the value specified by "[dmi/id]product_name" */ + if (udev_resolve_subsys_kernel(attr, vbuf, sizeof(vbuf), true) == 0) + val = vbuf; + + /* try to read the attribute the device */ + if (!val) + (void) sd_device_get_sysattr_value(dev, attr, &val); + + /* try to read the attribute of the parent device, other matches have selected */ + if (!val && event->dev_parent && event->dev_parent != dev) + (void) sd_device_get_sysattr_value(event->dev_parent, attr, &val); + + if (!val) + goto null_terminate; + + /* strip trailing whitespace, and replace unwanted characters */ + if (val != vbuf) + strscpy_full(vbuf, sizeof(vbuf), val, &truncated); + delete_trailing_chars(vbuf, NULL); + count = udev_replace_chars(vbuf, UDEV_ALLOWED_CHARS_INPUT); + if (count > 0) + log_device_debug(dev, "%i character(s) replaced", count); + strpcpy_full(&s, l, vbuf, &t); + truncated = truncated || t; + break; + } + case FORMAT_SUBST_PARENT: + r = sd_device_get_parent(dev, &parent); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + r = sd_device_get_devname(parent, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + strpcpy_full(&s, l, val + STRLEN("/dev/"), &truncated); + break; + case FORMAT_SUBST_DEVNODE: + r = sd_device_get_devname(dev, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + case FORMAT_SUBST_NAME: + if (event->name) + strpcpy_full(&s, l, event->name, &truncated); + else if (sd_device_get_devname(dev, &val) >= 0) + strpcpy_full(&s, l, val + STRLEN("/dev/"), &truncated); + else { + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + } + break; + case FORMAT_SUBST_LINKS: + FOREACH_DEVICE_DEVLINK(dev, link) { + if (s == dest) + strpcpy_full(&s, l, link + STRLEN("/dev/"), &truncated); + else + strpcpyl_full(&s, l, &truncated, " ", link + STRLEN("/dev/"), NULL); + if (truncated) + break; + } + if (s == dest) + goto null_terminate; + break; + case FORMAT_SUBST_ROOT: + strpcpy_full(&s, l, "/dev", &truncated); + break; + case FORMAT_SUBST_SYS: + strpcpy_full(&s, l, "/sys", &truncated); + break; + case FORMAT_SUBST_ENV: + if (isempty(attr)) + return -EINVAL; + r = device_get_property_value_with_fallback(dev, attr, global_props, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + strpcpy_full(&s, l, val, &truncated); + break; + default: + assert_not_reached(); + } + + if (ret_truncated) + *ret_truncated = truncated; + + return s - dest; + +null_terminate: + if (ret_truncated) + *ret_truncated = truncated; + + *s = '\0'; + return 0; +} + +size_t udev_event_apply_format( + UdevEvent *event, + const char *src, + char *dest, + size_t size, + bool replace_whitespace, + Hashmap *global_props, + bool *ret_truncated) { + + bool truncated = false; + const char *s = ASSERT_PTR(src); + int r; + + assert(event); + assert(event->dev); + assert(dest); + assert(size > 0); + + while (*s) { + FormatSubstitutionType type; + char attr[UDEV_PATH_SIZE]; + ssize_t subst_len; + bool t; + + r = get_subst_type(&s, false, &type, attr); + if (r < 0) { + log_device_warning_errno(event->dev, r, "Invalid format string, ignoring: %s", src); + break; + } else if (r == 0) { + if (size < 2) { + /* need space for this char and the terminating NUL */ + truncated = true; + break; + } + *dest++ = *s++; + size--; + continue; + } + + subst_len = udev_event_subst_format(event, type, attr, dest, size, global_props, &t); + if (subst_len < 0) { + log_device_warning_errno(event->dev, subst_len, + "Failed to substitute variable '$%s' or apply format '%%%c', ignoring: %m", + format_type_to_string(type), format_type_to_char(type)); + break; + } + + truncated = truncated || t; + + /* FORMAT_SUBST_RESULT handles spaces itself */ + if (replace_whitespace && type != FORMAT_SUBST_RESULT) + /* udev_replace_whitespace can replace in-place, + * and does nothing if subst_len == 0 */ + subst_len = udev_replace_whitespace(dest, dest, subst_len); + + dest += subst_len; + size -= subst_len; + } + + assert(size >= 1); + + if (ret_truncated) + *ret_truncated = truncated; + + *dest = '\0'; + return size; +} + +int udev_check_format(const char *value, size_t *offset, const char **hint) { + FormatSubstitutionType type; + const char *s = value; + char attr[UDEV_PATH_SIZE]; + int r; + + while (*s) { + r = get_subst_type(&s, true, &type, attr); + if (r < 0) { + if (offset) + *offset = s - value; + if (hint) + *hint = "invalid substitution type"; + return r; + } else if (r == 0) { + s++; + continue; + } + + if (IN_SET(type, FORMAT_SUBST_ATTR, FORMAT_SUBST_ENV) && isempty(attr)) { + if (offset) + *offset = s - value; + if (hint) + *hint = "attribute value missing"; + return -EINVAL; + } + + if (type == FORMAT_SUBST_RESULT && !isempty(attr)) { + unsigned i; + + r = safe_atou_optional_plus(attr, &i); + if (r < 0) { + if (offset) + *offset = s - value; + if (hint) + *hint = "attribute value not a valid number"; + return r; + } + } + } + + return 0; +} + +int udev_resolve_subsys_kernel(const char *string, char *result, size_t maxsize, bool read_value) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + _cleanup_free_ char *temp = NULL; + char *subsys, *sysname, *attr; + const char *val; + int r; + + assert(string); + assert(result); + + /* handle "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */ + + if (string[0] != '[') + return -EINVAL; + + temp = strdup(string); + if (!temp) + return -ENOMEM; + + subsys = &temp[1]; + + sysname = strchr(subsys, '/'); + if (!sysname) + return -EINVAL; + sysname[0] = '\0'; + sysname = &sysname[1]; + + attr = strchr(sysname, ']'); + if (!attr) + return -EINVAL; + attr[0] = '\0'; + attr = &attr[1]; + if (attr[0] == '/') + attr = &attr[1]; + if (attr[0] == '\0') + attr = NULL; + + if (read_value && !attr) + return -EINVAL; + + r = sd_device_new_from_subsystem_sysname(&dev, subsys, sysname); + if (r < 0) + return r; + + if (read_value) { + r = sd_device_get_sysattr_value(dev, attr, &val); + if (r < 0 && !ERRNO_IS_PRIVILEGE(r) && r != -ENOENT) + return r; + if (r >= 0) + strscpy(result, maxsize, val); + else + result[0] = '\0'; + log_debug("value '[%s/%s]%s' is '%s'", subsys, sysname, attr, result); + } else { + r = sd_device_get_syspath(dev, &val); + if (r < 0) + return r; + + strscpyl(result, maxsize, val, attr ? "/" : NULL, attr ?: NULL, NULL); + log_debug("path '[%s/%s]%s' is '%s'", subsys, sysname, strempty(attr), result); + } + return 0; +} diff --git a/src/udev/udev-format.h b/src/udev/udev-format.h new file mode 100644 index 0000000..92fef9b --- /dev/null +++ b/src/udev/udev-format.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> +#include <stddef.h> + +typedef struct UdevEvent UdevEvent; + +#define UDEV_ALLOWED_CHARS_INPUT "/ $%?," + +size_t udev_event_apply_format( + UdevEvent *event, + const char *src, + char *dest, + size_t size, + bool replace_whitespace, + Hashmap *global_props, + bool *ret_truncated); +int udev_check_format(const char *value, size_t *offset, const char **hint); + +int udev_resolve_subsys_kernel(const char *string, char *result, size_t maxsize, bool read_value); diff --git a/src/udev/udev-manager.c b/src/udev/udev-manager.c new file mode 100644 index 0000000..8077e51 --- /dev/null +++ b/src/udev/udev-manager.c @@ -0,0 +1,1352 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "blockdev-util.h" +#include "cgroup-util.h" +#include "common-signal.h" +#include "cpu-set-util.h" +#include "daemon-util.h" +#include "device-monitor-private.h" +#include "device-private.h" +#include "device-util.h" +#include "errno-list.h" +#include "event-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "hashmap.h" +#include "inotify-util.h" +#include "iovec-util.h" +#include "limits-util.h" +#include "list.h" +#include "mkdir.h" +#include "process-util.h" +#include "selinux-util.h" +#include "signal-util.h" +#include "socket-util.h" +#include "string-util.h" +#include "syslog-util.h" +#include "udev-builtin.h" +#include "udev-ctrl.h" +#include "udev-event.h" +#include "udev-manager.h" +#include "udev-node.h" +#include "udev-spawn.h" +#include "udev-trace.h" +#include "udev-util.h" +#include "udev-watch.h" +#include "udev-worker.h" + +#define WORKER_NUM_MAX UINT64_C(2048) + +#define EVENT_RETRY_INTERVAL_USEC (200 * USEC_PER_MSEC) +#define EVENT_RETRY_TIMEOUT_USEC (3 * USEC_PER_MINUTE) + +typedef enum EventState { + EVENT_UNDEF, + EVENT_QUEUED, + EVENT_RUNNING, +} EventState; + +typedef struct Event { + Manager *manager; + Worker *worker; + EventState state; + + sd_device *dev; + + sd_device_action_t action; + uint64_t seqnum; + uint64_t blocker_seqnum; + const char *id; + const char *devpath; + const char *devpath_old; + const char *devnode; + + /* Used when the device is locked by another program. */ + usec_t retry_again_next_usec; + usec_t retry_again_timeout_usec; + sd_event_source *retry_event_source; + + sd_event_source *timeout_warning_event; + sd_event_source *timeout_event; + + LIST_FIELDS(Event, event); +} Event; + +typedef enum WorkerState { + WORKER_UNDEF, + WORKER_RUNNING, + WORKER_IDLE, + WORKER_KILLED, + WORKER_KILLING, +} WorkerState; + +typedef struct Worker { + Manager *manager; + pid_t pid; + sd_event_source *child_event_source; + sd_device_monitor *monitor; + WorkerState state; + Event *event; +} Worker; + +static Event *event_free(Event *event) { + if (!event) + return NULL; + + assert(event->manager); + + LIST_REMOVE(event, event->manager->events, event); + sd_device_unref(event->dev); + + sd_event_source_unref(event->retry_event_source); + sd_event_source_unref(event->timeout_warning_event); + sd_event_source_unref(event->timeout_event); + + if (event->worker) + event->worker->event = NULL; + + return mfree(event); +} + +static void event_queue_cleanup(Manager *manager, EventState match_state) { + LIST_FOREACH(event, event, manager->events) { + if (match_state != EVENT_UNDEF && match_state != event->state) + continue; + + event_free(event); + } +} + +static Worker *worker_free(Worker *worker) { + if (!worker) + return NULL; + + if (worker->manager) + hashmap_remove(worker->manager->workers, PID_TO_PTR(worker->pid)); + + sd_event_source_unref(worker->child_event_source); + sd_device_monitor_unref(worker->monitor); + event_free(worker->event); + + return mfree(worker); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Worker*, worker_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(worker_hash_op, void, trivial_hash_func, trivial_compare_func, Worker, worker_free); + +Manager* manager_free(Manager *manager) { + if (!manager) + return NULL; + + udev_builtin_exit(); + + hashmap_free_free_free(manager->properties); + udev_rules_free(manager->rules); + + hashmap_free(manager->workers); + event_queue_cleanup(manager, EVENT_UNDEF); + + safe_close(manager->inotify_fd); + safe_close_pair(manager->worker_watch); + + sd_device_monitor_unref(manager->monitor); + udev_ctrl_unref(manager->ctrl); + + sd_event_source_unref(manager->inotify_event); + sd_event_source_unref(manager->kill_workers_event); + sd_event_source_unref(manager->memory_pressure_event_source); + sd_event_source_unref(manager->sigrtmin18_event_source); + sd_event_unref(manager->event); + + free(manager->cgroup); + return mfree(manager); +} + +static int on_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata); + +static int worker_new(Worker **ret, Manager *manager, sd_device_monitor *worker_monitor, pid_t pid) { + _cleanup_(worker_freep) Worker *worker = NULL; + int r; + + assert(ret); + assert(manager); + assert(worker_monitor); + assert(pid > 1); + + /* close monitor, but keep address around */ + device_monitor_disconnect(worker_monitor); + + worker = new(Worker, 1); + if (!worker) + return -ENOMEM; + + *worker = (Worker) { + .monitor = sd_device_monitor_ref(worker_monitor), + .pid = pid, + }; + + r = sd_event_add_child(manager->event, &worker->child_event_source, pid, WEXITED, on_sigchld, worker); + if (r < 0) + return r; + + r = hashmap_ensure_put(&manager->workers, &worker_hash_op, PID_TO_PTR(pid), worker); + if (r < 0) + return r; + + worker->manager = manager; + + *ret = TAKE_PTR(worker); + return 0; +} + +static void manager_kill_workers(Manager *manager, bool force) { + Worker *worker; + + assert(manager); + + HASHMAP_FOREACH(worker, manager->workers) { + if (worker->state == WORKER_KILLED) + continue; + + if (worker->state == WORKER_RUNNING && !force) { + worker->state = WORKER_KILLING; + continue; + } + + worker->state = WORKER_KILLED; + (void) kill(worker->pid, SIGTERM); + } +} + +static void manager_exit(Manager *manager) { + assert(manager); + + manager->exit = true; + + (void) sd_notify(/* unset= */ false, NOTIFY_STOPPING); + + /* close sources of new events and discard buffered events */ + manager->ctrl = udev_ctrl_unref(manager->ctrl); + + manager->inotify_event = sd_event_source_disable_unref(manager->inotify_event); + manager->inotify_fd = safe_close(manager->inotify_fd); + + manager->monitor = sd_device_monitor_unref(manager->monitor); + + /* discard queued events and kill workers */ + event_queue_cleanup(manager, EVENT_QUEUED); + manager_kill_workers(manager, true); +} + +static void notify_ready(Manager *manager) { + int r; + + assert(manager); + + r = sd_notifyf(/* unset= */ false, + "READY=1\n" + "STATUS=Processing with %u children at max", manager->children_max); + if (r < 0) + log_warning_errno(r, "Failed to send readiness notification, ignoring: %m"); +} + +/* reload requested, HUP signal received, rules changed, builtin changed */ +static void manager_reload(Manager *manager, bool force) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + usec_t now_usec; + int r; + + assert(manager); + + assert_se(sd_event_now(manager->event, CLOCK_MONOTONIC, &now_usec) >= 0); + if (!force && now_usec < usec_add(manager->last_usec, 3 * USEC_PER_SEC)) + /* check for changed config, every 3 seconds at most */ + return; + manager->last_usec = now_usec; + + /* Reload SELinux label database, to make the child inherit the up-to-date database. */ + mac_selinux_maybe_reload(); + + /* Nothing changed. It is not necessary to reload. */ + if (!udev_rules_should_reload(manager->rules) && !udev_builtin_should_reload()) { + + if (!force) + return; + + /* If we eat this up, then tell our service manager to just continue */ + (void) sd_notifyf(/* unset= */ false, + "RELOADING=1\n" + "STATUS=Skipping configuration reloading, nothing changed.\n" + "MONOTONIC_USEC=" USEC_FMT, now(CLOCK_MONOTONIC)); + } else { + (void) sd_notifyf(/* unset= */ false, + "RELOADING=1\n" + "STATUS=Flushing configuration...\n" + "MONOTONIC_USEC=" USEC_FMT, now(CLOCK_MONOTONIC)); + + manager_kill_workers(manager, false); + + udev_builtin_exit(); + udev_builtin_init(); + + r = udev_rules_load(&rules, manager->resolve_name_timing); + if (r < 0) + log_warning_errno(r, "Failed to read udev rules, using the previously loaded rules, ignoring: %m"); + else + udev_rules_free_and_replace(manager->rules, rules); + } + + notify_ready(manager); +} + +static int on_kill_workers_event(sd_event_source *s, uint64_t usec, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + + log_debug("Cleanup idle workers"); + manager_kill_workers(manager, false); + + return 1; +} + +static int on_event_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + Event *event = ASSERT_PTR(userdata); + + assert(event->manager); + assert(event->worker); + + kill_and_sigcont(event->worker->pid, event->manager->timeout_signal); + event->worker->state = WORKER_KILLED; + + log_device_error(event->dev, "Worker ["PID_FMT"] processing SEQNUM=%"PRIu64" killed", event->worker->pid, event->seqnum); + + return 1; +} + +static int on_event_timeout_warning(sd_event_source *s, uint64_t usec, void *userdata) { + Event *event = ASSERT_PTR(userdata); + + assert(event->worker); + + log_device_warning(event->dev, "Worker ["PID_FMT"] processing SEQNUM=%"PRIu64" is taking a long time", event->worker->pid, event->seqnum); + + return 1; +} + +static void worker_attach_event(Worker *worker, Event *event) { + Manager *manager; + sd_event *e; + + assert(worker); + assert(worker->manager); + assert(event); + assert(!event->worker); + assert(!worker->event); + + worker->state = WORKER_RUNNING; + worker->event = event; + event->state = EVENT_RUNNING; + event->worker = worker; + + manager = worker->manager; + e = manager->event; + + (void) sd_event_add_time_relative(e, &event->timeout_warning_event, CLOCK_MONOTONIC, + udev_warn_timeout(manager->timeout_usec), USEC_PER_SEC, + on_event_timeout_warning, event); + + (void) sd_event_add_time_relative(e, &event->timeout_event, CLOCK_MONOTONIC, + manager->timeout_usec, USEC_PER_SEC, + on_event_timeout, event); +} + +static int worker_spawn(Manager *manager, Event *event) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *worker_monitor = NULL; + Worker *worker; + pid_t pid; + int r; + + /* listen for new events */ + r = device_monitor_new_full(&worker_monitor, MONITOR_GROUP_NONE, -1); + if (r < 0) + return r; + + (void) sd_device_monitor_set_description(worker_monitor, "worker"); + + /* allow the main daemon netlink address to send devices to the worker */ + r = device_monitor_allow_unicast_sender(worker_monitor, manager->monitor); + if (r < 0) + return log_error_errno(r, "Worker: Failed to set unicast sender: %m"); + + r = device_monitor_enable_receiving(worker_monitor); + if (r < 0) + return log_error_errno(r, "Worker: Failed to enable receiving of device: %m"); + + r = safe_fork("(udev-worker)", FORK_DEATHSIG_SIGTERM, &pid); + if (r < 0) { + event->state = EVENT_QUEUED; + return log_error_errno(r, "Failed to fork() worker: %m"); + } + if (r == 0) { + _cleanup_(udev_worker_done) UdevWorker w = { + .monitor = TAKE_PTR(worker_monitor), + .properties = TAKE_PTR(manager->properties), + .rules = TAKE_PTR(manager->rules), + .pipe_fd = TAKE_FD(manager->worker_watch[WRITE_END]), + .inotify_fd = TAKE_FD(manager->inotify_fd), + .exec_delay_usec = manager->exec_delay_usec, + .timeout_usec = manager->timeout_usec, + .timeout_signal = manager->timeout_signal, + .log_level = manager->log_level, + .blockdev_read_only = manager->blockdev_read_only, + }; + + /* Worker process */ + r = udev_worker_main(&w, event->dev); + log_close(); + _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); + } + + r = worker_new(&worker, manager, worker_monitor, pid); + if (r < 0) + return log_error_errno(r, "Failed to create worker object: %m"); + + worker_attach_event(worker, event); + + log_device_debug(event->dev, "Worker ["PID_FMT"] is forked for processing SEQNUM=%"PRIu64".", pid, event->seqnum); + return 0; +} + +static int event_run(Event *event) { + static bool log_children_max_reached = true; + Manager *manager; + Worker *worker; + int r; + + assert(event); + assert(event->manager); + + log_device_uevent(event->dev, "Device ready for processing"); + + (void) event_source_disable(event->retry_event_source); + + manager = event->manager; + HASHMAP_FOREACH(worker, manager->workers) { + if (worker->state != WORKER_IDLE) + continue; + + r = device_monitor_send_device(manager->monitor, worker->monitor, event->dev); + if (r < 0) { + log_device_error_errno(event->dev, r, "Worker ["PID_FMT"] did not accept message, killing the worker: %m", + worker->pid); + (void) kill(worker->pid, SIGKILL); + worker->state = WORKER_KILLED; + continue; + } + worker_attach_event(worker, event); + return 1; /* event is now processing. */ + } + + if (hashmap_size(manager->workers) >= manager->children_max) { + /* Avoid spamming the debug logs if the limit is already reached and + * many events still need to be processed */ + if (log_children_max_reached && manager->children_max > 1) { + log_debug("Maximum number (%u) of children reached.", hashmap_size(manager->workers)); + log_children_max_reached = false; + } + return 0; /* no free worker */ + } + + /* Re-enable the debug message for the next batch of events */ + log_children_max_reached = true; + + /* start new worker and pass initial device */ + r = worker_spawn(manager, event); + if (r < 0) + return r; + + return 1; /* event is now processing. */ +} + +bool devpath_conflict(const char *a, const char *b) { + /* This returns true when two paths are equivalent, or one is a child of another. */ + + if (!a || !b) + return false; + + for (; *a != '\0' && *b != '\0'; a++, b++) + if (*a != *b) + return false; + + return *a == '/' || *b == '/' || *a == *b; +} + +static int event_is_blocked(Event *event) { + Event *loop_event = NULL; + int r; + + /* lookup event for identical, parent, child device */ + + assert(event); + assert(event->manager); + assert(event->blocker_seqnum <= event->seqnum); + + if (event->retry_again_next_usec > 0) { + usec_t now_usec; + + r = sd_event_now(event->manager->event, CLOCK_BOOTTIME, &now_usec); + if (r < 0) + return r; + + if (event->retry_again_next_usec > now_usec) + return true; + } + + if (event->blocker_seqnum == event->seqnum) + /* we have checked previously and no blocker found */ + return false; + + LIST_FOREACH(event, e, event->manager->events) { + loop_event = e; + + /* we already found a later event, earlier cannot block us, no need to check again */ + if (loop_event->seqnum < event->blocker_seqnum) + continue; + + /* event we checked earlier still exists, no need to check again */ + if (loop_event->seqnum == event->blocker_seqnum) + return true; + + /* found ourself, no later event can block us */ + if (loop_event->seqnum >= event->seqnum) + goto no_blocker; + + /* found event we have not checked */ + break; + } + + assert(loop_event); + assert(loop_event->seqnum > event->blocker_seqnum && + loop_event->seqnum < event->seqnum); + + /* check if queue contains events we depend on */ + LIST_FOREACH(event, e, loop_event) { + loop_event = e; + + /* found ourself, no later event can block us */ + if (loop_event->seqnum >= event->seqnum) + goto no_blocker; + + if (streq_ptr(loop_event->id, event->id)) + break; + + if (devpath_conflict(event->devpath, loop_event->devpath) || + devpath_conflict(event->devpath, loop_event->devpath_old) || + devpath_conflict(event->devpath_old, loop_event->devpath)) + break; + + if (event->devnode && streq_ptr(event->devnode, loop_event->devnode)) + break; + } + + assert(loop_event); + + log_device_debug(event->dev, "SEQNUM=%" PRIu64 " blocked by SEQNUM=%" PRIu64, + event->seqnum, loop_event->seqnum); + + event->blocker_seqnum = loop_event->seqnum; + return true; + +no_blocker: + event->blocker_seqnum = event->seqnum; + return false; +} + +static int event_queue_start(Manager *manager) { + int r; + + assert(manager); + + if (!manager->events || manager->exit || manager->stop_exec_queue) + return 0; + + /* To make the stack directory /run/udev/links cleaned up later. */ + manager->udev_node_needs_cleanup = true; + + r = event_source_disable(manager->kill_workers_event); + if (r < 0) + log_warning_errno(r, "Failed to disable event source for cleaning up idle workers, ignoring: %m"); + + manager_reload(manager, /* force = */ false); + + LIST_FOREACH(event, event, manager->events) { + if (event->state != EVENT_QUEUED) + continue; + + /* do not start event if parent or child event is still running or queued */ + r = event_is_blocked(event); + if (r > 0) + continue; + if (r < 0) + log_device_warning_errno(event->dev, r, + "Failed to check dependencies for event (SEQNUM=%"PRIu64", ACTION=%s), " + "assuming there is no blocking event, ignoring: %m", + event->seqnum, + strna(device_action_to_string(event->action))); + + r = event_run(event); + if (r <= 0) /* 0 means there are no idle workers. Let's escape from the loop. */ + return r; + } + + return 0; +} + +static int on_event_retry(sd_event_source *s, uint64_t usec, void *userdata) { + /* This does nothing. The on_post() callback will start the event if there exists an idle worker. */ + return 1; +} + +static int event_requeue(Event *event) { + usec_t now_usec; + int r; + + assert(event); + assert(event->manager); + assert(event->manager->event); + + event->timeout_warning_event = sd_event_source_disable_unref(event->timeout_warning_event); + event->timeout_event = sd_event_source_disable_unref(event->timeout_event); + + /* add a short delay to suppress busy loop */ + r = sd_event_now(event->manager->event, CLOCK_BOOTTIME, &now_usec); + if (r < 0) + return log_device_warning_errno(event->dev, r, + "Failed to get current time, " + "skipping event (SEQNUM=%"PRIu64", ACTION=%s): %m", + event->seqnum, strna(device_action_to_string(event->action))); + + if (event->retry_again_timeout_usec > 0 && event->retry_again_timeout_usec <= now_usec) + return log_device_warning_errno(event->dev, SYNTHETIC_ERRNO(ETIMEDOUT), + "The underlying block device is locked by a process more than %s, " + "skipping event (SEQNUM=%"PRIu64", ACTION=%s).", + FORMAT_TIMESPAN(EVENT_RETRY_TIMEOUT_USEC, USEC_PER_MINUTE), + event->seqnum, strna(device_action_to_string(event->action))); + + event->retry_again_next_usec = usec_add(now_usec, EVENT_RETRY_INTERVAL_USEC); + if (event->retry_again_timeout_usec == 0) + event->retry_again_timeout_usec = usec_add(now_usec, EVENT_RETRY_TIMEOUT_USEC); + + r = event_reset_time_relative(event->manager->event, &event->retry_event_source, + CLOCK_MONOTONIC, EVENT_RETRY_INTERVAL_USEC, 0, + on_event_retry, NULL, + 0, "retry-event", true); + if (r < 0) + return log_device_warning_errno(event->dev, r, "Failed to reset timer event source for retrying event, " + "skipping event (SEQNUM=%"PRIu64", ACTION=%s): %m", + event->seqnum, strna(device_action_to_string(event->action))); + + if (event->worker && event->worker->event == event) + event->worker->event = NULL; + event->worker = NULL; + + event->state = EVENT_QUEUED; + return 0; +} + +static int event_queue_assume_block_device_unlocked(Manager *manager, sd_device *dev) { + const char *devname; + int r; + + /* When a new event for a block device is queued or we get an inotify event, assume that the + * device is not locked anymore. The assumption may not be true, but that should not cause any + * issues, as in that case events will be requeued soon. */ + + r = udev_get_whole_disk(dev, NULL, &devname); + if (r <= 0) + return r; + + LIST_FOREACH(event, event, manager->events) { + const char *event_devname; + + if (event->state != EVENT_QUEUED) + continue; + + if (event->retry_again_next_usec == 0) + continue; + + if (udev_get_whole_disk(event->dev, NULL, &event_devname) <= 0) + continue; + + if (!streq(devname, event_devname)) + continue; + + event->retry_again_next_usec = 0; + } + + return 0; +} + +static int event_queue_insert(Manager *manager, sd_device *dev) { + const char *devpath, *devpath_old = NULL, *id = NULL, *devnode = NULL; + sd_device_action_t action; + uint64_t seqnum; + Event *event; + int r; + + assert(manager); + assert(dev); + + /* We only accepts devices received by device monitor. */ + r = sd_device_get_seqnum(dev, &seqnum); + if (r < 0) + return r; + + r = sd_device_get_action(dev, &action); + if (r < 0) + return r; + + r = sd_device_get_devpath(dev, &devpath); + if (r < 0) + return r; + + r = sd_device_get_property_value(dev, "DEVPATH_OLD", &devpath_old); + if (r < 0 && r != -ENOENT) + return r; + + r = device_get_device_id(dev, &id); + if (r < 0 && r != -ENOENT) + return r; + + r = sd_device_get_devname(dev, &devnode); + if (r < 0 && r != -ENOENT) + return r; + + event = new(Event, 1); + if (!event) + return -ENOMEM; + + *event = (Event) { + .manager = manager, + .dev = sd_device_ref(dev), + .seqnum = seqnum, + .action = action, + .id = id, + .devpath = devpath, + .devpath_old = devpath_old, + .devnode = devnode, + .state = EVENT_QUEUED, + }; + + if (!manager->events) { + r = touch("/run/udev/queue"); + if (r < 0) + log_warning_errno(r, "Failed to touch /run/udev/queue, ignoring: %m"); + } + + LIST_APPEND(event, manager->events, event); + + log_device_uevent(dev, "Device is queued"); + + return 0; +} + +static int on_uevent(sd_device_monitor *monitor, sd_device *dev, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + int r; + + DEVICE_TRACE_POINT(kernel_uevent_received, dev); + + device_ensure_usec_initialized(dev, NULL); + + r = event_queue_insert(manager, dev); + if (r < 0) { + log_device_error_errno(dev, r, "Failed to insert device into event queue: %m"); + return 1; + } + + (void) event_queue_assume_block_device_unlocked(manager, dev); + + return 1; +} + +static int on_worker(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + + for (;;) { + EventResult result; + struct iovec iovec = IOVEC_MAKE(&result, sizeof(result)); + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; + struct msghdr msghdr = { + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + ssize_t size; + struct ucred *ucred; + Worker *worker; + + size = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT); + if (size == -EINTR) + continue; + if (size == -EAGAIN) + /* nothing more to read */ + break; + if (size < 0) + return log_error_errno(size, "Failed to receive message: %m"); + + cmsg_close_all(&msghdr); + + if (size != sizeof(result)) { + log_warning("Ignoring worker message with invalid size %zi bytes", size); + continue; + } + + ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred); + if (!ucred || ucred->pid <= 0) { + log_warning("Ignoring worker message without valid PID"); + continue; + } + + /* lookup worker who sent the signal */ + worker = hashmap_get(manager->workers, PID_TO_PTR(ucred->pid)); + if (!worker) { + log_debug("Worker ["PID_FMT"] returned, but is no longer tracked", ucred->pid); + continue; + } + + if (worker->state == WORKER_KILLING) { + worker->state = WORKER_KILLED; + (void) kill(worker->pid, SIGTERM); + } else if (worker->state != WORKER_KILLED) + worker->state = WORKER_IDLE; + + /* worker returned */ + if (result == EVENT_RESULT_TRY_AGAIN && + event_requeue(worker->event) < 0) + udev_broadcast_result(manager->monitor, worker->event->dev, -ETIMEDOUT); + + /* When event_requeue() succeeds, worker->event is NULL, and event_free() handles NULL gracefully. */ + event_free(worker->event); + } + + return 1; +} + +static void manager_set_default_children_max(Manager *manager) { + uint64_t cpu_limit, mem_limit, cpu_count = 1; + int r; + + assert(manager); + + if (manager->children_max != 0) + return; + + r = cpus_in_affinity_mask(); + if (r < 0) + log_warning_errno(r, "Failed to determine number of local CPUs, ignoring: %m"); + else + cpu_count = r; + + cpu_limit = cpu_count * 2 + 16; + mem_limit = MAX(physical_memory() / (128*1024*1024), UINT64_C(10)); + + manager->children_max = MIN3(cpu_limit, mem_limit, WORKER_NUM_MAX); + log_debug("Set children_max to %u", manager->children_max); +} + +/* receive the udevd message from userspace */ +static int on_ctrl_msg(UdevCtrl *uctrl, UdevCtrlMessageType type, const UdevCtrlMessageValue *value, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + int r; + + assert(value); + + switch (type) { + case UDEV_CTRL_SET_LOG_LEVEL: + if ((value->intval & LOG_PRIMASK) != value->intval) { + log_debug("Received invalid udev control message (SET_LOG_LEVEL, %i), ignoring.", value->intval); + break; + } + + log_debug("Received udev control message (SET_LOG_LEVEL), setting log_level=%i", value->intval); + + r = log_get_max_level(); + if (r == value->intval) + break; + + log_set_max_level(value->intval); + manager->log_level = value->intval; + manager_kill_workers(manager, false); + break; + case UDEV_CTRL_STOP_EXEC_QUEUE: + log_debug("Received udev control message (STOP_EXEC_QUEUE)"); + manager->stop_exec_queue = true; + break; + case UDEV_CTRL_START_EXEC_QUEUE: + log_debug("Received udev control message (START_EXEC_QUEUE)"); + manager->stop_exec_queue = false; + /* It is not necessary to call event_queue_start() here, as it will be called in on_post() if necessary. */ + break; + case UDEV_CTRL_RELOAD: + log_debug("Received udev control message (RELOAD)"); + manager_reload(manager, /* force = */ true); + break; + case UDEV_CTRL_SET_ENV: { + _unused_ _cleanup_free_ char *old_val = NULL, *old_key = NULL; + _cleanup_free_ char *key = NULL, *val = NULL; + const char *eq; + + eq = strchr(value->buf, '='); + if (!eq) { + log_error("Invalid key format '%s'", value->buf); + return 1; + } + + key = strndup(value->buf, eq - value->buf); + if (!key) { + log_oom(); + return 1; + } + + old_val = hashmap_remove2(manager->properties, key, (void **) &old_key); + + r = hashmap_ensure_allocated(&manager->properties, &string_hash_ops); + if (r < 0) { + log_oom(); + return 1; + } + + eq++; + if (isempty(eq)) + log_debug("Received udev control message (ENV), unsetting '%s'", key); + else { + val = strdup(eq); + if (!val) { + log_oom(); + return 1; + } + + log_debug("Received udev control message (ENV), setting '%s=%s'", key, val); + + r = hashmap_put(manager->properties, key, val); + if (r < 0) { + log_oom(); + return 1; + } + } + + key = val = NULL; + manager_kill_workers(manager, false); + break; + } + case UDEV_CTRL_SET_CHILDREN_MAX: + if (value->intval < 0) { + log_debug("Received invalid udev control message (SET_MAX_CHILDREN, %i), ignoring.", value->intval); + return 0; + } + + log_debug("Received udev control message (SET_MAX_CHILDREN), setting children_max=%i", value->intval); + manager->children_max = value->intval; + + /* When 0 is specified, determine the maximum based on the system resources. */ + manager_set_default_children_max(manager); + + notify_ready(manager); + break; + case UDEV_CTRL_PING: + log_debug("Received udev control message (PING)"); + break; + case UDEV_CTRL_EXIT: + log_debug("Received udev control message (EXIT)"); + manager_exit(manager); + break; + default: + log_debug("Received unknown udev control message, ignoring"); + } + + return 1; +} + +static int synthesize_change_one(sd_device *dev, sd_device *target) { + int r; + + if (DEBUG_LOGGING) { + const char *syspath = NULL; + (void) sd_device_get_syspath(target, &syspath); + log_device_debug(dev, "device is closed, synthesising 'change' on %s", strna(syspath)); + } + + r = sd_device_trigger(target, SD_DEVICE_CHANGE); + if (r < 0) + return log_device_debug_errno(target, r, "Failed to trigger 'change' uevent: %m"); + + DEVICE_TRACE_POINT(synthetic_change_event, dev); + + return 0; +} + +static int synthesize_change(sd_device *dev) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + bool part_table_read; + const char *sysname; + int r, k; + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return r; + + if (startswith(sysname, "dm-") || block_device_is_whole_disk(dev) <= 0) + return synthesize_change_one(dev, dev); + + r = blockdev_reread_partition_table(dev); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to re-read partition table, ignoring: %m"); + part_table_read = r >= 0; + + /* search for partitions */ + r = partition_enumerator_new(dev, &e); + if (r < 0) + return r; + + /* We have partitions and re-read the table, the kernel already sent out a "change" + * event for the disk, and "remove/add" for all partitions. */ + if (part_table_read && sd_device_enumerator_get_device_first(e)) + return 0; + + /* We have partitions but re-reading the partition table did not work, synthesize + * "change" for the disk and all partitions. */ + r = synthesize_change_one(dev, dev); + FOREACH_DEVICE(e, d) { + k = synthesize_change_one(dev, d); + if (k < 0 && r >= 0) + r = k; + } + + return r; +} + +static int on_inotify(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + union inotify_event_buffer buffer; + ssize_t l; + int r; + + l = read(fd, &buffer, sizeof(buffer)); + if (l < 0) { + if (ERRNO_IS_TRANSIENT(errno)) + return 0; + + return log_error_errno(errno, "Failed to read inotify fd: %m"); + } + + FOREACH_INOTIFY_EVENT_WARN(e, buffer, l) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + const char *devnode; + + /* Do not handle IN_IGNORED here. Especially, do not try to call udev_watch_end() from the + * main process. Otherwise, the pair of the symlinks may become inconsistent, and several + * garbage may remain. The old symlinks are removed by a worker that processes the + * corresponding 'remove' uevent; + * udev_event_execute_rules() -> event_execute_rules_on_remove() -> udev_watch_end(). */ + + if (!FLAGS_SET(e->mask, IN_CLOSE_WRITE)) + continue; + + r = device_new_from_watch_handle(&dev, e->wd); + if (r < 0) { + /* Device may be removed just after closed. */ + log_debug_errno(r, "Failed to create sd_device object from watch handle, ignoring: %m"); + continue; + } + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) { + /* Also here, device may be already removed. */ + log_device_debug_errno(dev, r, "Failed to get device node, ignoring: %m"); + continue; + } + + log_device_debug(dev, "Received inotify event for %s.", devnode); + + (void) event_queue_assume_block_device_unlocked(manager, dev); + (void) synthesize_change(dev); + } + + return 0; +} + +static int on_sigterm(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + + manager_exit(manager); + + return 1; +} + +static int on_sighup(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + + manager_reload(manager, /* force = */ true); + + return 1; +} + +static int on_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata) { + Worker *worker = ASSERT_PTR(userdata); + Manager *manager = ASSERT_PTR(worker->manager); + sd_device *dev = worker->event ? ASSERT_PTR(worker->event->dev) : NULL; + EventResult result; + + assert(si); + + switch (si->si_code) { + case CLD_EXITED: + if (si->si_status == 0) + log_device_debug(dev, "Worker ["PID_FMT"] exited.", si->si_pid); + else + log_device_warning(dev, "Worker ["PID_FMT"] exited with return code %i.", + si->si_pid, si->si_status); + result = EVENT_RESULT_EXIT_STATUS_BASE + si->si_status; + break; + + case CLD_KILLED: + case CLD_DUMPED: + log_device_warning(dev, "Worker ["PID_FMT"] terminated by signal %i (%s).", + si->si_pid, si->si_status, signal_to_string(si->si_status)); + result = EVENT_RESULT_SIGNAL_BASE + si->si_status; + break; + + default: + assert_not_reached(); + } + + if (result != EVENT_RESULT_SUCCESS && dev) { + /* delete state from disk */ + device_delete_db(dev); + device_tag_index(dev, NULL, false); + + /* Forward kernel event to libudev listeners */ + udev_broadcast_result(manager->monitor, dev, result); + } + + worker_free(worker); + + return 1; +} + +static int on_post(sd_event_source *s, void *userdata) { + Manager *manager = ASSERT_PTR(userdata); + + if (manager->events) { + /* Try to process pending events if idle workers exist. Why is this necessary? + * When a worker finished an event and became idle, even if there was a pending event, + * the corresponding device might have been locked and the processing of the event + * delayed for a while, preventing the worker from processing the event immediately. + * Now, the device may be unlocked. Let's try again! */ + event_queue_start(manager); + return 1; + } + + /* There are no queued events. Let's remove /run/udev/queue and clean up the idle processes. */ + + if (unlink("/run/udev/queue") < 0) { + if (errno != ENOENT) + log_warning_errno(errno, "Failed to unlink /run/udev/queue, ignoring: %m"); + } else + log_debug("No events are queued, removing /run/udev/queue."); + + if (!hashmap_isempty(manager->workers)) { + /* There are idle workers */ + (void) event_reset_time_relative(manager->event, &manager->kill_workers_event, + CLOCK_MONOTONIC, 3 * USEC_PER_SEC, USEC_PER_SEC, + on_kill_workers_event, manager, + 0, "kill-workers-event", false); + return 1; + } + + /* There are no idle workers. */ + + if (manager->udev_node_needs_cleanup) { + (void) udev_node_cleanup(); + manager->udev_node_needs_cleanup = false; + } + + if (manager->exit) + return sd_event_exit(manager->event, 0); + + if (manager->cgroup) + /* cleanup possible left-over processes in our cgroup */ + (void) cg_kill(manager->cgroup, SIGKILL, CGROUP_IGNORE_SELF, /* set=*/ NULL, /* kill_log= */ NULL, /* userdata= */ NULL); + + return 1; +} + +Manager* manager_new(void) { + Manager *manager; + + manager = new(Manager, 1); + if (!manager) + return NULL; + + *manager = (Manager) { + .inotify_fd = -EBADF, + .worker_watch = EBADF_PAIR, + .log_level = LOG_INFO, + .resolve_name_timing = RESOLVE_NAME_EARLY, + .timeout_usec = 180 * USEC_PER_SEC, + .timeout_signal = SIGKILL, + }; + + return manager; +} + +int manager_init(Manager *manager, int fd_ctrl, int fd_uevent) { + _cleanup_free_ char *cgroup = NULL; + int r; + + assert(manager); + + r = udev_ctrl_new_from_fd(&manager->ctrl, fd_ctrl); + if (r < 0) + return log_error_errno(r, "Failed to initialize udev control socket: %m"); + + r = udev_ctrl_enable_receiving(manager->ctrl); + if (r < 0) + return log_error_errno(r, "Failed to bind udev control socket: %m"); + + r = device_monitor_new_full(&manager->monitor, MONITOR_GROUP_KERNEL, fd_uevent); + if (r < 0) + return log_error_errno(r, "Failed to initialize device monitor: %m"); + + (void) sd_device_monitor_set_description(manager->monitor, "manager"); + + r = device_monitor_enable_receiving(manager->monitor); + if (r < 0) + return log_error_errno(r, "Failed to bind netlink socket: %m"); + + manager->log_level = log_get_max_level(); + + r = cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, 0, &cgroup); + if (r < 0) + log_debug_errno(r, "Failed to get cgroup, ignoring: %m"); + else if (endswith(cgroup, "/udev")) { /* If we are in a subcgroup /udev/ we assume it was delegated to us */ + log_debug("Running in delegated subcgroup '%s'.", cgroup); + manager->cgroup = TAKE_PTR(cgroup); + } + + return 0; +} + +int manager_main(Manager *manager) { + int fd_worker, r; + + manager_set_default_children_max(manager); + + /* unnamed socket from workers to the main daemon */ + r = socketpair(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0, manager->worker_watch); + if (r < 0) + return log_error_errno(errno, "Failed to create socketpair for communicating with workers: %m"); + + fd_worker = manager->worker_watch[READ_END]; + + r = setsockopt_int(fd_worker, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return log_error_errno(r, "Failed to enable SO_PASSCRED: %m"); + + manager->inotify_fd = inotify_init1(IN_CLOEXEC); + if (manager->inotify_fd < 0) + return log_error_errno(errno, "Failed to create inotify descriptor: %m"); + + udev_watch_restore(manager->inotify_fd); + + /* block and listen to all signals on signalfd */ + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGHUP, SIGCHLD, SIGRTMIN+18, -1) >= 0); + + r = sd_event_default(&manager->event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGINT, on_sigterm, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGINT event source: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGTERM, on_sigterm, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGTERM event source: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGHUP, on_sighup, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGHUP event source: %m"); + + r = sd_event_set_watchdog(manager->event, true); + if (r < 0) + return log_error_errno(r, "Failed to create watchdog event source: %m"); + + r = udev_ctrl_attach_event(manager->ctrl, manager->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to udev control: %m"); + + r = udev_ctrl_start(manager->ctrl, on_ctrl_msg, manager); + if (r < 0) + return log_error_errno(r, "Failed to start udev control: %m"); + + /* This needs to be after the inotify and uevent handling, to make sure + * that the ping is send back after fully processing the pending uevents + * (including the synthetic ones we may create due to inotify events). + */ + r = sd_event_source_set_priority(udev_ctrl_get_event_source(manager->ctrl), SD_EVENT_PRIORITY_IDLE); + if (r < 0) + return log_error_errno(r, "Failed to set IDLE event priority for udev control event source: %m"); + + r = sd_event_add_io(manager->event, &manager->inotify_event, manager->inotify_fd, EPOLLIN, on_inotify, manager); + if (r < 0) + return log_error_errno(r, "Failed to create inotify event source: %m"); + + r = sd_device_monitor_attach_event(manager->monitor, manager->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to device monitor: %m"); + + r = sd_device_monitor_start(manager->monitor, on_uevent, manager); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + r = sd_event_add_io(manager->event, NULL, fd_worker, EPOLLIN, on_worker, manager); + if (r < 0) + return log_error_errno(r, "Failed to create worker event source: %m"); + + r = sd_event_add_post(manager->event, NULL, on_post, manager); + if (r < 0) + return log_error_errno(r, "Failed to create post event source: %m"); + + /* Eventually, we probably want to do more here on memory pressure, for example, kill idle workers immediately */ + r = sd_event_add_memory_pressure(manager->event, &manager->memory_pressure_event_source, NULL, NULL); + if (r < 0) + log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) || ERRNO_IS_PRIVILEGE(r) || (r == -EHOSTDOWN) ? LOG_DEBUG : LOG_WARNING, r, + "Failed to allocate memory pressure watch, ignoring: %m"); + + r = sd_event_add_signal(manager->event, &manager->memory_pressure_event_source, SIGRTMIN+18, sigrtmin18_handler, NULL); + if (r < 0) + return log_error_errno(r, "Failed to allocate SIGRTMIN+18 event source, ignoring: %m"); + + manager->last_usec = now(CLOCK_MONOTONIC); + + udev_builtin_init(); + + r = udev_rules_load(&manager->rules, manager->resolve_name_timing); + if (r < 0) + return log_error_errno(r, "Failed to read udev rules: %m"); + + r = udev_rules_apply_static_dev_perms(manager->rules); + if (r < 0) + log_warning_errno(r, "Failed to apply permissions on static device nodes, ignoring: %m"); + + notify_ready(manager); + + r = sd_event_loop(manager->event); + if (r < 0) + log_error_errno(r, "Event loop failed: %m"); + + (void) sd_notify(/* unset= */ false, NOTIFY_STOPPING); + return r; +} diff --git a/src/udev/udev-manager.h b/src/udev/udev-manager.h new file mode 100644 index 0000000..afbc67f --- /dev/null +++ b/src/udev/udev-manager.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "hashmap.h" +#include "macro.h" +#include "time-util.h" +#include "udev-ctrl.h" +#include "udev-rules.h" + +typedef struct Event Event; +typedef struct Worker Worker; + +typedef struct Manager { + sd_event *event; + Hashmap *workers; + LIST_HEAD(Event, events); + char *cgroup; + int log_level; + + UdevRules *rules; + Hashmap *properties; + + sd_device_monitor *monitor; + UdevCtrl *ctrl; + int worker_watch[2]; + + /* used by udev-watch */ + int inotify_fd; + sd_event_source *inotify_event; + + sd_event_source *kill_workers_event; + + sd_event_source *memory_pressure_event_source; + sd_event_source *sigrtmin18_event_source; + + usec_t last_usec; + + ResolveNameTiming resolve_name_timing; + unsigned children_max; + usec_t exec_delay_usec; + usec_t timeout_usec; + int timeout_signal; + bool blockdev_read_only; + + bool udev_node_needs_cleanup; + bool stop_exec_queue; + bool exit; +} Manager; + +Manager* manager_new(void); +Manager* manager_free(Manager *manager); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_init(Manager *manager, int fd_ctrl, int fd_uevent); +int manager_main(Manager *manager); + +bool devpath_conflict(const char *a, const char *b); diff --git a/src/udev/udev-node.c b/src/udev/udev-node.c new file mode 100644 index 0000000..e12c26c --- /dev/null +++ b/src/udev/udev-node.c @@ -0,0 +1,790 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <sys/file.h> + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "device-private.h" +#include "device-util.h" +#include "devnum-util.h" +#include "dirent-util.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "label-util.h" +#include "mkdir-label.h" +#include "parse-util.h" +#include "path-util.h" +#include "selinux-util.h" +#include "smack-util.h" +#include "stat-util.h" +#include "string-util.h" +#include "udev-node.h" +#include "user-util.h" + +#define UDEV_NODE_HASH_KEY SD_ID128_MAKE(b9,6a,f1,ce,40,31,44,1a,9e,19,ec,8b,ae,f3,e3,2f) + +int udev_node_cleanup(void) { + _cleanup_closedir_ DIR *dir = NULL; + + /* This must not be called when any workers exist. It would cause a race between mkdir() called + * by stack_directory_lock() and unlinkat() called by this. */ + + dir = opendir("/run/udev/links"); + if (!dir) { + if (errno == ENOENT) + return 0; + + return log_debug_errno(errno, "Failed to open directory '/run/udev/links', ignoring: %m"); + } + + FOREACH_DIRENT_ALL(de, dir, break) { + _cleanup_free_ char *lockfile = NULL; + + if (de->d_name[0] == '.') + continue; + + if (de->d_type != DT_DIR) + continue; + + /* As commented in the above, this is called when no worker exists, hence the file is not + * locked. On a later uevent, the lock file will be created if necessary. So, we can safely + * remove the file now. */ + lockfile = path_join(de->d_name, ".lock"); + if (!lockfile) + return log_oom_debug(); + + if (unlinkat(dirfd(dir), lockfile, 0) < 0 && errno != ENOENT) { + log_debug_errno(errno, "Failed to remove '/run/udev/links/%s', ignoring: %m", lockfile); + continue; + } + + if (unlinkat(dirfd(dir), de->d_name, AT_REMOVEDIR) < 0 && errno != ENOTEMPTY) + log_debug_errno(errno, "Failed to remove '/run/udev/links/%s', ignoring: %m", de->d_name); + } + + return 0; +} + +static int node_symlink(sd_device *dev, const char *devnode, const char *slink) { + struct stat st; + int r; + + assert(dev); + assert(slink); + + if (!devnode) { + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device node: %m"); + } + + if (lstat(slink, &st) >= 0) { + if (!S_ISLNK(st.st_mode)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EEXIST), + "Conflicting inode '%s' found, symlink to '%s' will not be created.", + slink, devnode); + } else if (errno != ENOENT) + return log_device_debug_errno(dev, errno, "Failed to lstat() '%s': %m", slink); + + r = mkdir_parents_label(slink, 0755); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to create parent directory of '%s': %m", slink); + + /* use relative link */ + r = symlink_atomic_full_label(devnode, slink, /* make_relative = */ true); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to create symlink '%s' to '%s': %m", slink, devnode); + + log_device_debug(dev, "Successfully created symlink '%s' to '%s'", slink, devnode); + return 0; +} + +static int stack_directory_read_one(int dirfd, const char *id, char **devnode, int *priority) { + _cleanup_free_ char *buf = NULL; + int tmp_prio, r; + + assert(dirfd >= 0); + assert(id); + assert(priority); + + /* This reads priority and device node from the symlink under /run/udev/links (or udev database). + * If 'devnode' is NULL, obtained priority is always set to '*priority'. If 'devnode' is non-NULL, + * this updates '*devnode' and '*priority'. */ + + /* First, let's try to read the entry with the new format, which should replace the old format pretty + * quickly. */ + r = readlinkat_malloc(dirfd, id, &buf); + if (r >= 0) { + char *colon; + + /* With the new format, the devnode and priority can be obtained from symlink itself. */ + + colon = strchr(buf, ':'); + if (!colon || colon == buf) + return -EINVAL; + + *colon = '\0'; + + /* Of course, this check is racy, but it is not necessary to be perfect. Even if the device + * node will be removed after this check, we will receive 'remove' uevent, and the invalid + * symlink will be removed during processing the event. The check is just for shortening the + * timespan that the symlink points to a non-existing device node. */ + if (access(colon + 1, F_OK) < 0) + return -ENODEV; + + r = safe_atoi(buf, &tmp_prio); + if (r < 0) + return r; + + if (!devnode) + goto finalize; + + if (*devnode && tmp_prio <= *priority) + return 0; /* Unchanged */ + + r = free_and_strdup(devnode, colon + 1); + if (r < 0) + return r; + + } else if (r == -EINVAL) { /* Not a symlink ? try the old format */ + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + const char *val; + + /* Old format. The devnode and priority must be obtained from uevent and udev database. */ + + r = sd_device_new_from_device_id(&dev, id); + if (r < 0) + return r; + + r = device_get_devlink_priority(dev, &tmp_prio); + if (r < 0) + return r; + + if (!devnode) + goto finalize; + + if (*devnode && tmp_prio <= *priority) + return 0; /* Unchanged */ + + r = sd_device_get_devname(dev, &val); + if (r < 0) + return r; + + r = free_and_strdup(devnode, val); + if (r < 0) + return r; + + } else + return r == -ENOENT ? -ENODEV : r; + +finalize: + *priority = tmp_prio; + return 1; /* Updated */ +} + +static int stack_directory_find_prioritized_devnode(sd_device *dev, int dirfd, bool add, char **ret) { + _cleanup_closedir_ DIR *dir = NULL; + _cleanup_free_ char *devnode = NULL; + int r, priority; + const char *id; + + assert(dev); + assert(dirfd >= 0); + assert(ret); + + /* Find device node of device with highest priority. This returns 1 if a device found, 0 if no + * device found, or a negative errno on error. */ + + if (add) { + const char *n; + + r = device_get_devlink_priority(dev, &priority); + if (r < 0) + return r; + + r = sd_device_get_devname(dev, &n); + if (r < 0) + return r; + + devnode = strdup(n); + if (!devnode) + return -ENOMEM; + } + + dir = xopendirat(dirfd, ".", O_NOFOLLOW); + if (!dir) + return -errno; + + r = device_get_device_id(dev, &id); + if (r < 0) + return r; + + FOREACH_DIRENT(de, dir, break) { + + /* skip ourself */ + if (streq(de->d_name, id)) + continue; + + r = stack_directory_read_one(dirfd, de->d_name, &devnode, &priority); + if (r < 0 && r != -ENODEV) + log_debug_errno(r, "Failed to read '%s', ignoring: %m", de->d_name); + } + + *ret = TAKE_PTR(devnode); + return !!*ret; +} + +static int stack_directory_update(sd_device *dev, int fd, bool add) { + const char *id; + int r; + + assert(dev); + assert(fd >= 0); + + r = device_get_device_id(dev, &id); + if (r < 0) + return r; + + if (add) { + _cleanup_free_ char *data = NULL, *buf = NULL; + const char *devname; + int priority; + + r = sd_device_get_devname(dev, &devname); + if (r < 0) + return r; + + r = device_get_devlink_priority(dev, &priority); + if (r < 0) + return r; + + if (asprintf(&data, "%i:%s", priority, devname) < 0) + return -ENOMEM; + + if (readlinkat_malloc(fd, id, &buf) >= 0 && streq(buf, data)) + return 0; /* Unchanged. */ + + (void) unlinkat(fd, id, 0); + + if (symlinkat(data, fd, id) < 0) + return -errno; + + } else { + if (unlinkat(fd, id, 0) < 0) { + if (errno == ENOENT) + return 0; /* Unchanged. */ + return -errno; + } + } + + return 1; /* Updated. */ +} + +size_t udev_node_escape_path(const char *src, char *dest, size_t size) { + size_t i, j; + uint64_t h; + + assert(src); + assert(dest); + assert(size >= 12); + + for (i = 0, j = 0; src[i] != '\0'; i++) { + if (src[i] == '/') { + if (j+4 >= size - 12 + 1) + goto toolong; + memcpy(&dest[j], "\\x2f", 4); + j += 4; + } else if (src[i] == '\\') { + if (j+4 >= size - 12 + 1) + goto toolong; + memcpy(&dest[j], "\\x5c", 4); + j += 4; + } else { + if (j+1 >= size - 12 + 1) + goto toolong; + dest[j] = src[i]; + j++; + } + } + dest[j] = '\0'; + return j; + +toolong: + /* If the input path is too long to encode as a filename, then let's suffix with a string + * generated from the hash of the path. */ + + h = siphash24_string(src, UDEV_NODE_HASH_KEY.bytes); + + for (unsigned k = 0; k <= 10; k++) + dest[size - k - 2] = urlsafe_base64char((h >> (k * 6)) & 63); + + dest[size - 1] = '\0'; + return size - 1; +} + +static int stack_directory_get_name(const char *slink, char **ret) { + _cleanup_free_ char *s = NULL, *dirname = NULL; + char name_enc[NAME_MAX+1]; + const char *name; + int r; + + assert(slink); + assert(ret); + + r = path_simplify_alloc(slink, &s); + if (r < 0) + return r; + + if (!path_is_normalized(s)) + return -EINVAL; + + name = path_startswith(s, "/dev"); + if (empty_or_root(name)) + return -EINVAL; + + udev_node_escape_path(name, name_enc, sizeof(name_enc)); + + dirname = path_join("/run/udev/links", name_enc); + if (!dirname) + return -ENOMEM; + + *ret = TAKE_PTR(dirname); + return 0; +} + +static int stack_directory_open(sd_device *dev, const char *slink, int *ret_dirfd, int *ret_lockfd) { + _cleanup_close_ int dirfd = -EBADF, lockfd = -EBADF; + _cleanup_free_ char *dirname = NULL; + int r; + + assert(dev); + assert(slink); + assert(ret_dirfd); + assert(ret_lockfd); + + r = stack_directory_get_name(slink, &dirname); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to build stack directory name for '%s': %m", slink); + + r = mkdir_parents(dirname, 0755); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to create stack directory '%s': %m", dirname); + + dirfd = open_mkdir_at(AT_FDCWD, dirname, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_RDONLY, 0755); + if (dirfd < 0) + return log_device_debug_errno(dev, dirfd, "Failed to open stack directory '%s': %m", dirname); + + lockfd = openat(dirfd, ".lock", O_CLOEXEC | O_NOFOLLOW | O_RDONLY | O_CREAT, 0600); + if (lockfd < 0) + return log_device_debug_errno(dev, errno, "Failed to create lock file for stack directory '%s': %m", dirname); + + if (flock(lockfd, LOCK_EX) < 0) + return log_device_debug_errno(dev, errno, "Failed to place a lock on lock file for %s: %m", dirname); + + *ret_dirfd = TAKE_FD(dirfd); + *ret_lockfd = TAKE_FD(lockfd); + return 0; +} + +static int node_get_current(const char *slink, int dirfd, char **ret_id, int *ret_prio) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + _cleanup_free_ char *id_dup = NULL; + const char *id; + int r; + + assert(slink); + assert(dirfd >= 0); + assert(ret_id); + + r = sd_device_new_from_devname(&dev, slink); + if (r < 0) + return r; + + r = device_get_device_id(dev, &id); + if (r < 0) + return r; + + id_dup = strdup(id); + if (!id_dup) + return -ENOMEM; + + if (ret_prio) { + r = stack_directory_read_one(dirfd, id, NULL, ret_prio); + if (r < 0) + return r; + } + + *ret_id = TAKE_PTR(id_dup); + return 0; +} + +static int link_update(sd_device *dev, const char *slink, bool add) { + _cleanup_free_ char *current_id = NULL, *devnode = NULL; + _cleanup_close_ int dirfd = -EBADF, lockfd = -EBADF; + int r, current_prio; + + assert(dev); + assert(slink); + + r = stack_directory_open(dev, slink, &dirfd, &lockfd); + if (r < 0) + return r; + + r = node_get_current(slink, dirfd, ¤t_id, add ? ¤t_prio : NULL); + if (r < 0 && !ERRNO_IS_DEVICE_ABSENT(r)) + return log_device_debug_errno(dev, r, "Failed to get the current device node priority for '%s': %m", slink); + + r = stack_directory_update(dev, dirfd, add); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to update stack directory for '%s': %m", slink); + + if (current_id) { + const char *id; + + r = device_get_device_id(dev, &id); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device id: %m"); + + if (add) { + int prio; + + r = device_get_devlink_priority(dev, &prio); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devlink priority: %m"); + + if (streq(current_id, id)) { + if (current_prio <= prio) + /* The devlink is ours and already exists, and the new priority is + * equal or higher than the previous. Hence, it is not necessary to + * recreate it. */ + return 0; + + /* The devlink priority is downgraded. Another device may have a higher + * priority now. Let's find the device node with the highest priority. */ + } else { + if (current_prio > prio) + /* The devlink with a higher priority already exists and is owned by + * another device. Hence, it is not necessary to recreate it. */ + return 0; + + /* This device has the equal or a higher priority than the current. Let's + * create the devlink to our device node. */ + return node_symlink(dev, NULL, slink); + } + + } else { + if (!streq(current_id, id)) + /* The devlink already exists and is owned by another device. Hence, it is + * not necessary to recreate it. */ + return 0; + + /* The current devlink is ours, and the target device will be removed. Hence, we need + * to search the device that has the highest priority. and update the devlink. */ + } + } else { + /* The requested devlink does not exist, or the target device does not exist and the devlink + * points to a non-existing device. Let's search the device that has the highest priority, + * and update the devlink. */ + ; + } + + r = stack_directory_find_prioritized_devnode(dev, dirfd, add, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to determine device node with the highest priority for '%s': %m", slink); + if (r > 0) + return node_symlink(dev, devnode, slink); + + log_device_debug(dev, "No reference left for '%s', removing", slink); + + if (unlink(slink) < 0 && errno != ENOENT) + log_device_debug_errno(dev, errno, "Failed to remove '%s', ignoring: %m", slink); + + (void) rmdir_parents(slink, "/dev"); + + return 0; +} + +static int device_get_devpath_by_devnum(sd_device *dev, char **ret) { + const char *subsystem; + dev_t devnum; + int r; + + assert(dev); + assert(ret); + + r = sd_device_get_subsystem(dev, &subsystem); + if (r < 0) + return r; + + r = sd_device_get_devnum(dev, &devnum); + if (r < 0) + return r; + + return device_path_make_major_minor(streq(subsystem, "block") ? S_IFBLK : S_IFCHR, devnum, ret); +} + +int udev_node_update(sd_device *dev, sd_device *dev_old) { + _cleanup_free_ char *filename = NULL; + int r; + + assert(dev); + assert(dev_old); + + /* update possible left-over symlinks */ + FOREACH_DEVICE_DEVLINK(dev_old, devlink) { + /* check if old link name still belongs to this device */ + if (device_has_devlink(dev, devlink)) + continue; + + log_device_debug(dev, + "Removing/updating old device symlink '%s', which is no longer belonging to this device.", + devlink); + + r = link_update(dev, devlink, /* add = */ false); + if (r < 0) + log_device_warning_errno(dev, r, + "Failed to remove/update device symlink '%s', ignoring: %m", + devlink); + } + + /* create/update symlinks, add symlinks to name index */ + FOREACH_DEVICE_DEVLINK(dev, devlink) { + r = link_update(dev, devlink, /* add = */ true); + if (r < 0) + log_device_warning_errno(dev, r, + "Failed to create/update device symlink '%s', ignoring: %m", + devlink); + } + + r = device_get_devpath_by_devnum(dev, &filename); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device path: %m"); + + /* always add /dev/{block,char}/$major:$minor */ + r = node_symlink(dev, NULL, filename); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to create device symlink '%s': %m", filename); + + return 0; +} + +int udev_node_remove(sd_device *dev) { + _cleanup_free_ char *filename = NULL; + int r; + + assert(dev); + + /* remove/update symlinks, remove symlinks from name index */ + FOREACH_DEVICE_DEVLINK(dev, devlink) { + r = link_update(dev, devlink, /* add = */ false); + if (r < 0) + log_device_warning_errno(dev, r, + "Failed to remove/update device symlink '%s', ignoring: %m", + devlink); + } + + r = device_get_devpath_by_devnum(dev, &filename); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device path: %m"); + + /* remove /dev/{block,char}/$major:$minor */ + if (unlink(filename) < 0 && errno != ENOENT) + return log_device_debug_errno(dev, errno, "Failed to remove '%s': %m", filename); + + return 0; +} + +static int udev_node_apply_permissions_impl( + sd_device *dev, /* can be NULL, only used for logging. */ + int node_fd, + const char *devnode, + bool apply_mac, + mode_t mode, + uid_t uid, + gid_t gid, + OrderedHashmap *seclabel_list) { + + bool apply_mode, apply_uid, apply_gid; + struct stat stats; + int r; + + assert(node_fd >= 0); + assert(devnode); + + if (fstat(node_fd, &stats) < 0) + return log_device_debug_errno(dev, errno, "cannot stat() node %s: %m", devnode); + + /* If group is set, but mode is not set, "upgrade" mode for the group. */ + if (mode == MODE_INVALID && gid_is_valid(gid) && gid > 0) + mode = 0660; + + apply_mode = mode != MODE_INVALID && (stats.st_mode & 0777) != (mode & 0777); + apply_uid = uid_is_valid(uid) && stats.st_uid != uid; + apply_gid = gid_is_valid(gid) && stats.st_gid != gid; + + if (apply_mode || apply_uid || apply_gid || apply_mac) { + bool selinux = false, smack = false; + const char *name, *label; + + if (apply_mode || apply_uid || apply_gid) { + log_device_debug(dev, "Setting permissions %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o", + devnode, + uid_is_valid(uid) ? uid : stats.st_uid, + gid_is_valid(gid) ? gid : stats.st_gid, + mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); + + r = fchmod_and_chown(node_fd, mode, uid, gid); + if (r < 0) + log_device_full_errno(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, + "Failed to set owner/mode of %s to uid=" UID_FMT + ", gid=" GID_FMT ", mode=%#o: %m", + devnode, + uid_is_valid(uid) ? uid : stats.st_uid, + gid_is_valid(gid) ? gid : stats.st_gid, + mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); + } else + log_device_debug(dev, "Preserve permissions of %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o", + devnode, + uid_is_valid(uid) ? uid : stats.st_uid, + gid_is_valid(gid) ? gid : stats.st_gid, + mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); + + /* apply SECLABEL{$module}=$label */ + ORDERED_HASHMAP_FOREACH_KEY(label, name, seclabel_list) { + int q; + + if (streq(name, "selinux")) { + selinux = true; + + q = mac_selinux_apply_fd(node_fd, devnode, label); + if (q < 0) + log_device_full_errno(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q, + "SECLABEL: failed to set SELinux label '%s': %m", label); + else + log_device_debug(dev, "SECLABEL: set SELinux label '%s'", label); + + } else if (streq(name, "smack")) { + smack = true; + + q = mac_smack_apply_fd(node_fd, SMACK_ATTR_ACCESS, label); + if (q < 0) + log_device_full_errno(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q, + "SECLABEL: failed to set SMACK label '%s': %m", label); + else + log_device_debug(dev, "SECLABEL: set SMACK label '%s'", label); + + } else + log_device_error(dev, "SECLABEL: unknown subsystem, ignoring '%s'='%s'", name, label); + } + + /* set the defaults */ + if (!selinux) + (void) mac_selinux_fix_full(node_fd, NULL, devnode, LABEL_IGNORE_ENOENT); + if (!smack) + (void) mac_smack_apply_fd(node_fd, SMACK_ATTR_ACCESS, NULL); + } + + /* always update timestamp when we re-use the node, like on media change events */ + r = futimens_opath(node_fd, NULL); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to adjust timestamp of node %s: %m", devnode); + + return 0; +} + +int udev_node_apply_permissions( + sd_device *dev, + bool apply_mac, + mode_t mode, + uid_t uid, + gid_t gid, + OrderedHashmap *seclabel_list) { + + const char *devnode; + _cleanup_close_ int node_fd = -EBADF; + int r; + + assert(dev); + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devname: %m"); + + node_fd = sd_device_open(dev, O_PATH|O_CLOEXEC); + if (node_fd < 0) { + if (ERRNO_IS_DEVICE_ABSENT(node_fd)) { + log_device_debug_errno(dev, node_fd, "Device node %s is missing, skipping handling.", devnode); + return 0; /* This is necessarily racey, so ignore missing the device */ + } + + return log_device_debug_errno(dev, node_fd, "Cannot open node %s: %m", devnode); + } + + return udev_node_apply_permissions_impl(dev, node_fd, devnode, apply_mac, mode, uid, gid, seclabel_list); +} + +int static_node_apply_permissions( + const char *name, + mode_t mode, + uid_t uid, + gid_t gid, + char **tags) { + + _cleanup_free_ char *unescaped_filename = NULL; + _cleanup_close_ int node_fd = -EBADF; + const char *devnode; + struct stat stats; + int r; + + assert(name); + + if (uid == UID_INVALID && gid == GID_INVALID && mode == MODE_INVALID && !tags) + return 0; + + devnode = strjoina("/dev/", name); + + node_fd = open(devnode, O_PATH|O_CLOEXEC); + if (node_fd < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open %s: %m", devnode); + return 0; + } + + if (fstat(node_fd, &stats) < 0) + return log_error_errno(errno, "Failed to stat %s: %m", devnode); + + if (!S_ISBLK(stats.st_mode) && !S_ISCHR(stats.st_mode)) { + log_warning("%s is neither block nor character device, ignoring.", devnode); + return 0; + } + + if (!strv_isempty(tags)) { + unescaped_filename = xescape(name, "/."); + if (!unescaped_filename) + return log_oom(); + } + + /* export the tags to a directory as symlinks, allowing otherwise dead nodes to be tagged */ + STRV_FOREACH(t, tags) { + _cleanup_free_ char *p = NULL; + + p = path_join("/run/udev/static_node-tags/", *t, unescaped_filename); + if (!p) + return log_oom(); + + r = mkdir_parents(p, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directory for %s: %m", p); + + r = symlink(devnode, p); + if (r < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create symlink %s -> %s: %m", p, devnode); + } + + return udev_node_apply_permissions_impl(NULL, node_fd, devnode, false, mode, uid, gid, NULL); +} diff --git a/src/udev/udev-node.h b/src/udev/udev-node.h new file mode 100644 index 0000000..0c545e4 --- /dev/null +++ b/src/udev/udev-node.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-device.h" + +#include "hashmap.h" + +int udev_node_apply_permissions( + sd_device *dev, + bool apply_mac, + mode_t mode, + uid_t uid, + gid_t gid, + OrderedHashmap *seclabel_list); +int static_node_apply_permissions( + const char *name, + mode_t mode, + uid_t uid, + gid_t gid, + char **tags); + +int udev_node_remove(sd_device *dev); +int udev_node_update(sd_device *dev, sd_device *dev_old); +int udev_node_cleanup(void); + +size_t udev_node_escape_path(const char *src, char *dest, size_t size); diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c new file mode 100644 index 0000000..5f12002 --- /dev/null +++ b/src/udev/udev-rules.c @@ -0,0 +1,2965 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <ctype.h> + +#include "alloc-util.h" +#include "architecture.h" +#include "conf-files.h" +#include "conf-parser.h" +#include "confidential-virt.h" +#include "constants.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "glob-util.h" +#include "list.h" +#include "mkdir.h" +#include "netif-naming-scheme.h" +#include "nulstr-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "socket-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "strxcpyx.h" +#include "sysctl-util.h" +#include "syslog-util.h" +#include "udev-builtin.h" +#include "udev-event.h" +#include "udev-format.h" +#include "udev-node.h" +#include "udev-rules.h" +#include "udev-spawn.h" +#include "udev-trace.h" +#include "udev-util.h" +#include "user-util.h" +#include "virt.h" + +#define RULES_DIRS ((const char* const*) CONF_PATHS_STRV("udev/rules.d")) + +typedef enum { + OP_MATCH, /* == */ + OP_NOMATCH, /* != */ + OP_ADD, /* += */ + OP_REMOVE, /* -= */ + OP_ASSIGN, /* = */ + OP_ASSIGN_FINAL, /* := */ + _OP_TYPE_MAX, + _OP_TYPE_INVALID = -EINVAL, +} UdevRuleOperatorType; + +typedef enum { + MATCH_TYPE_EMPTY, /* empty string */ + MATCH_TYPE_PLAIN, /* no special characters */ + MATCH_TYPE_PLAIN_WITH_EMPTY, /* no special characters with empty string, e.g., "|foo" */ + MATCH_TYPE_GLOB, /* shell globs ?,*,[] */ + MATCH_TYPE_GLOB_WITH_EMPTY, /* shell globs ?,*,[] with empty string, e.g., "|foo*" */ + MATCH_TYPE_SUBSYSTEM, /* "subsystem", "bus", or "class" */ + _MATCH_TYPE_MAX, + _MATCH_TYPE_INVALID = -EINVAL, +} UdevRuleMatchType; + +typedef enum { + SUBST_TYPE_PLAIN, /* no substitution */ + SUBST_TYPE_FORMAT, /* % or $ */ + SUBST_TYPE_SUBSYS, /* "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */ + _SUBST_TYPE_MAX, + _SUBST_TYPE_INVALID = -EINVAL, +} UdevRuleSubstituteType; + +typedef enum { + /* lvalues which take match or nomatch operator */ + TK_M_ACTION, /* string, device_get_action() */ + TK_M_DEVPATH, /* path, sd_device_get_devpath() */ + TK_M_KERNEL, /* string, sd_device_get_sysname() */ + TK_M_DEVLINK, /* strv, sd_device_get_devlink_first(), sd_device_get_devlink_next() */ + TK_M_NAME, /* string, name of network interface */ + TK_M_ENV, /* string, device property, takes key through attribute */ + TK_M_CONST, /* string, system-specific hard-coded constant */ + TK_M_TAG, /* strv, sd_device_get_tag_first(), sd_device_get_tag_next() */ + TK_M_SUBSYSTEM, /* string, sd_device_get_subsystem() */ + TK_M_DRIVER, /* string, sd_device_get_driver() */ + TK_M_ATTR, /* string, takes filename through attribute, sd_device_get_sysattr_value(), udev_resolve_subsys_kernel(), etc. */ + TK_M_SYSCTL, /* string, takes kernel parameter through attribute */ + + /* matches parent parameters */ + TK_M_PARENTS_KERNEL, /* string */ + TK_M_PARENTS_SUBSYSTEM, /* string */ + TK_M_PARENTS_DRIVER, /* string */ + TK_M_PARENTS_ATTR, /* string */ + TK_M_PARENTS_TAG, /* strv */ + + TK_M_TEST, /* path, optionally mode_t can be specified by attribute, test the existence of a file */ + TK_M_PROGRAM, /* string, execute a program */ + TK_M_IMPORT_FILE, /* path */ + TK_M_IMPORT_PROGRAM, /* string, import properties from the result of program */ + TK_M_IMPORT_BUILTIN, /* string, import properties from the result of built-in command */ + TK_M_IMPORT_DB, /* string, import properties from database */ + TK_M_IMPORT_CMDLINE, /* string, kernel command line */ + TK_M_IMPORT_PARENT, /* string, parent property */ + TK_M_RESULT, /* string, result of TK_M_PROGRAM */ + +#define _TK_M_MAX (TK_M_RESULT + 1) +#define _TK_A_MIN _TK_M_MAX + + /* lvalues which take one of assign operators */ + TK_A_OPTIONS_STRING_ESCAPE_NONE, /* no argument */ + TK_A_OPTIONS_STRING_ESCAPE_REPLACE, /* no argument */ + TK_A_OPTIONS_DB_PERSIST, /* no argument */ + TK_A_OPTIONS_INOTIFY_WATCH, /* boolean */ + TK_A_OPTIONS_DEVLINK_PRIORITY, /* int */ + TK_A_OPTIONS_LOG_LEVEL, /* string of log level or "reset" */ + TK_A_OWNER, /* user name */ + TK_A_GROUP, /* group name */ + TK_A_MODE, /* mode string */ + TK_A_OWNER_ID, /* uid_t */ + TK_A_GROUP_ID, /* gid_t */ + TK_A_MODE_ID, /* mode_t */ + TK_A_TAG, /* string */ + TK_A_OPTIONS_STATIC_NODE, /* device path, /dev/... */ + TK_A_SECLABEL, /* string with attribute */ + TK_A_ENV, /* string with attribute */ + TK_A_NAME, /* ifname */ + TK_A_DEVLINK, /* string */ + TK_A_ATTR, /* string with attribute */ + TK_A_SYSCTL, /* string with attribute */ + TK_A_RUN_BUILTIN, /* string */ + TK_A_RUN_PROGRAM, /* string */ + + _TK_TYPE_MAX, + _TK_TYPE_INVALID = -EINVAL, +} UdevRuleTokenType; + +typedef enum { + LINE_HAS_NAME = 1 << 0, /* has NAME= */ + LINE_HAS_DEVLINK = 1 << 1, /* has SYMLINK=, OWNER=, GROUP= or MODE= */ + LINE_HAS_STATIC_NODE = 1 << 2, /* has OPTIONS=static_node */ + LINE_HAS_GOTO = 1 << 3, /* has GOTO= */ + LINE_HAS_LABEL = 1 << 4, /* has LABEL= */ + LINE_UPDATE_SOMETHING = 1 << 5, /* has other TK_A_* or TK_M_IMPORT tokens */ + LINE_IS_REFERENCED = 1 << 6, /* is referenced by GOTO */ +} UdevRuleLineType; + +typedef struct UdevRuleFile UdevRuleFile; +typedef struct UdevRuleLine UdevRuleLine; +typedef struct UdevRuleToken UdevRuleToken; + +struct UdevRuleToken { + UdevRuleTokenType type:8; + UdevRuleOperatorType op:8; + UdevRuleMatchType match_type:8; + UdevRuleSubstituteType attr_subst_type:7; + bool attr_match_remove_trailing_whitespace:1; + const char *value; + void *data; + + UdevRuleLine *rule_line; + LIST_FIELDS(UdevRuleToken, tokens); +}; + +struct UdevRuleLine { + char *line; + unsigned line_number; + UdevRuleLineType type; + + const char *label; + const char *goto_label; + UdevRuleLine *goto_line; + + UdevRuleFile *rule_file; + LIST_HEAD(UdevRuleToken, tokens); + LIST_FIELDS(UdevRuleLine, rule_lines); +}; + +struct UdevRuleFile { + char *filename; + unsigned issues; /* used by "udevadm verify" */ + + UdevRules *rules; + LIST_HEAD(UdevRuleLine, rule_lines); + LIST_FIELDS(UdevRuleFile, rule_files); +}; + +struct UdevRules { + ResolveNameTiming resolve_name_timing; + Hashmap *known_users; + Hashmap *known_groups; + Hashmap *stats_by_path; + LIST_HEAD(UdevRuleFile, rule_files); +}; + +#define LINE_GET_RULES(line) \ + ASSERT_PTR(ASSERT_PTR(ASSERT_PTR(line)->rule_file)->rules) + +/*** Logging helpers ***/ + +#define log_udev_rule_internal(device, file, line_nr, level, error, fmt, ...) \ + ({ \ + int _lv = (level); \ + sd_device *_dev = (device); \ + UdevRuleFile *_f = (file); \ + const char *_n = _f ? _f->filename : NULL; \ + \ + if (!_dev && _f) \ + _f->issues |= (1U << _lv); \ + \ + log_device_full_errno_zerook( \ + _dev, _lv, error, "%s:%u " fmt, \ + strna(_n), line_nr, \ + ##__VA_ARGS__); \ + }) + +/* Mainly used when applying tokens to the event device. */ +#define log_event_full_errno_zerook(device, token, ...) \ + ({ \ + UdevRuleToken *_t = (token); \ + UdevRuleLine *_l = _t ? _t->rule_line : NULL; \ + \ + log_udev_rule_internal( \ + device, \ + _l ? _l->rule_file : NULL, \ + _l ? _l->line_number : 0, \ + __VA_ARGS__); \ + }) + +#define log_event_full_errno(device, token, level, error, ...) \ + ({ \ + int _error = (error); \ + ASSERT_NON_ZERO(_error); \ + log_event_full_errno_zerook( \ + device, token, level, _error, ##__VA_ARGS__); \ + }) + +#define log_event_full(device, token, level, ...) (void) log_event_full_errno_zerook(device, token, level, 0, __VA_ARGS__) + +#define log_event_debug(device, token, ...) log_event_full(device, token, LOG_DEBUG, __VA_ARGS__) +#define log_event_info(device, token, ...) log_event_full(device, token, LOG_INFO, __VA_ARGS__) +#define log_event_notice(device, token, ...) log_event_full(device, token, LOG_NOTICE, __VA_ARGS__) +#define log_event_warning(device, token, ...) log_event_full(device, token, LOG_WARNING, __VA_ARGS__) +#define log_event_error(device, token, ...) log_event_full(device, token, LOG_ERR, __VA_ARGS__) + +#define log_event_debug_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_DEBUG, error, __VA_ARGS__) +#define log_event_info_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_INFO, error, __VA_ARGS__) +#define log_event_notice_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_NOTICE, error, __VA_ARGS__) +#define log_event_warning_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_WARNING, error, __VA_ARGS__) +#define log_event_error_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_ERR, error, __VA_ARGS__) + +/* Mainly used when parsing .rules files. */ +#define log_file_full_errno_zerook(...) \ + log_udev_rule_internal(NULL, __VA_ARGS__) + +#define log_file_error(file, line_nr, ...) \ + log_file_full_errno_zerook(file, line_nr, LOG_ERR, 0, __VA_ARGS__) + +#define log_line_full_errno_zerook(line, ...) \ + ({ \ + UdevRuleLine *_l = (line); \ + log_file_full_errno_zerook( \ + _l ? _l->rule_file : NULL, \ + _l ? _l->line_number : 0, \ + __VA_ARGS__); \ + }) + +#define log_line_full_errno(line, level, error, ...) \ + ({ \ + int _error = (error); \ + ASSERT_NON_ZERO(_error); \ + log_line_full_errno_zerook( \ + line, level, _error, ##__VA_ARGS__); \ + }) + +#define log_line_full(line, level, ...) (void) log_line_full_errno_zerook(line, level, 0, __VA_ARGS__) + +#define log_line_debug(line, ...) log_line_full(line, LOG_DEBUG, __VA_ARGS__) +#define log_line_info(line, ...) log_line_full(line, LOG_INFO, __VA_ARGS__) +#define log_line_notice(line, ...) log_line_full(line, LOG_NOTICE, __VA_ARGS__) +#define log_line_warning(line, ...) log_line_full(line, LOG_WARNING, __VA_ARGS__) +#define log_line_error(line, ...) log_line_full(line, LOG_ERR, __VA_ARGS__) + +#define log_line_debug_errno(line, error, ...) log_line_full_errno(line, LOG_DEBUG, error, __VA_ARGS__) +#define log_line_info_errno(line, error, ...) log_line_full_errno(line, LOG_INFO, error, __VA_ARGS__) +#define log_line_notice_errno(line, error, ...) log_line_full_errno(line, LOG_NOTICE, error, __VA_ARGS__) +#define log_line_warning_errno(line, error, ...) log_line_full_errno(line, LOG_WARNING, error, __VA_ARGS__) +#define log_line_error_errno(line, error, ...) log_line_full_errno(line, LOG_ERR, error, __VA_ARGS__) + +#define _log_line_invalid_token(line, key, type) \ + log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \ + "Invalid %s for %s.", type, key) + +#define log_line_invalid_op(line, key) _log_line_invalid_token(line, key, "operator") +#define log_line_invalid_attr(line, key) _log_line_invalid_token(line, key, "attribute") + +#define log_line_invalid_attr_format(line, key, attr, offset, hint) \ + log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \ + "Invalid attribute \"%s\" for %s (char %zu: %s), ignoring.", \ + attr, key, offset, hint) +#define log_line_invalid_value(line, key, value, offset, hint) \ + log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \ + "Invalid value \"%s\" for %s (char %zu: %s), ignoring.", \ + value, key, offset, hint) + +static void log_unknown_owner(sd_device *dev, UdevRuleLine *line, int error, const char *entity, const char *name) { + assert(line); + ASSERT_NON_ZERO(error); + + if (IN_SET(abs(error), ENOENT, ESRCH)) + log_udev_rule_internal(dev, line->rule_file, line->line_number, LOG_ERR, error, + "Unknown %s '%s', ignoring.", entity, name); + else + log_udev_rule_internal(dev, line->rule_file, line->line_number, LOG_ERR, error, + "Failed to resolve %s '%s', ignoring: %m", entity, name); +} + +static void log_event_truncated( + sd_device *dev, + UdevRuleToken *token, + const char *what, + const char *format, + const char *key, + bool is_match) { + + if (is_match) + log_event_debug(dev, token, + "The %s is truncated while substituting into '%s', " + "assuming the %s key does not match.", + what, format, key); + else + log_event_warning(dev, token, + "The %s is truncated while substituting into '%s', " + "refusing to apply the %s key.", + what, format, key); +} + +/*** Other functions ***/ + +static UdevRuleToken *udev_rule_token_free(UdevRuleToken *token) { + if (!token) + return NULL; + + if (token->rule_line) + LIST_REMOVE(tokens, token->rule_line->tokens, token); + + return mfree(token); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleToken*, udev_rule_token_free); + +static void udev_rule_line_clear_tokens(UdevRuleLine *rule_line) { + assert(rule_line); + + LIST_FOREACH(tokens, i, rule_line->tokens) + udev_rule_token_free(i); +} + +static UdevRuleLine *udev_rule_line_free(UdevRuleLine *rule_line) { + if (!rule_line) + return NULL; + + udev_rule_line_clear_tokens(rule_line); + + if (rule_line->rule_file) + LIST_REMOVE(rule_lines, rule_line->rule_file->rule_lines, rule_line); + + free(rule_line->line); + return mfree(rule_line); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleLine*, udev_rule_line_free); + +static UdevRuleFile *udev_rule_file_free(UdevRuleFile *rule_file) { + if (!rule_file) + return NULL; + + LIST_FOREACH(rule_lines, i, rule_file->rule_lines) + udev_rule_line_free(i); + + if (rule_file->rules) + LIST_REMOVE(rule_files, rule_file->rules->rule_files, rule_file); + + free(rule_file->filename); + return mfree(rule_file); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleFile*, udev_rule_file_free); + +UdevRules *udev_rules_free(UdevRules *rules) { + if (!rules) + return NULL; + + LIST_FOREACH(rule_files, i, rules->rule_files) + udev_rule_file_free(i); + + hashmap_free_free_key(rules->known_users); + hashmap_free_free_key(rules->known_groups); + hashmap_free(rules->stats_by_path); + return mfree(rules); +} + +static int rule_resolve_user(UdevRuleLine *rule_line, const char *name, uid_t *ret) { + Hashmap **known_users = &LINE_GET_RULES(rule_line)->known_users; + _cleanup_free_ char *n = NULL; + uid_t uid; + void *val; + int r; + + assert(name); + assert(ret); + + val = hashmap_get(*known_users, name); + if (val) { + *ret = PTR_TO_UID(val); + return 0; + } + + r = get_user_creds(&name, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) { + log_unknown_owner(NULL, rule_line, r, "user", name); + *ret = UID_INVALID; + return 0; + } + + n = strdup(name); + if (!n) + return -ENOMEM; + + r = hashmap_ensure_put(known_users, &string_hash_ops, n, UID_TO_PTR(uid)); + if (r < 0) + return r; + + TAKE_PTR(n); + *ret = uid; + return 0; +} + +static int rule_resolve_group(UdevRuleLine *rule_line, const char *name, gid_t *ret) { + Hashmap **known_groups = &LINE_GET_RULES(rule_line)->known_groups; + _cleanup_free_ char *n = NULL; + gid_t gid; + void *val; + int r; + + assert(name); + assert(ret); + + val = hashmap_get(*known_groups, name); + if (val) { + *ret = PTR_TO_GID(val); + return 0; + } + + r = get_group_creds(&name, &gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) { + log_unknown_owner(NULL, rule_line, r, "group", name); + *ret = GID_INVALID; + return 0; + } + + n = strdup(name); + if (!n) + return -ENOMEM; + + r = hashmap_ensure_put(known_groups, &string_hash_ops, n, GID_TO_PTR(gid)); + if (r < 0) + return r; + + TAKE_PTR(n); + *ret = gid; + return 0; +} + +static UdevRuleSubstituteType rule_get_substitution_type(const char *str) { + assert(str); + + if (str[0] == '[') + return SUBST_TYPE_SUBSYS; + if (strchr(str, '%') || strchr(str, '$')) + return SUBST_TYPE_FORMAT; + return SUBST_TYPE_PLAIN; +} + +static bool type_has_nulstr_value(UdevRuleTokenType type) { + return type < TK_M_TEST || type == TK_M_RESULT; +} + +static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, UdevRuleOperatorType op, char *value, void *data) { + _cleanup_(udev_rule_token_freep) UdevRuleToken *token = NULL; + UdevRuleMatchType match_type = _MATCH_TYPE_INVALID; + UdevRuleSubstituteType subst_type = _SUBST_TYPE_INVALID; + bool remove_trailing_whitespace = false; + size_t len; + + assert(rule_line); + assert(type >= 0 && type < _TK_TYPE_MAX); + assert(op >= 0 && op < _OP_TYPE_MAX); + + if (type < _TK_M_MAX) { + assert(value); + assert(IN_SET(op, OP_MATCH, OP_NOMATCH)); + + if (type == TK_M_SUBSYSTEM && STR_IN_SET(value, "subsystem", "bus", "class")) + match_type = MATCH_TYPE_SUBSYSTEM; + else if (isempty(value)) + match_type = MATCH_TYPE_EMPTY; + else if (streq(value, "?*")) { + /* Convert KEY=="?*" -> KEY!="" */ + match_type = MATCH_TYPE_EMPTY; + op = op == OP_MATCH ? OP_NOMATCH : OP_MATCH; + } else if (string_is_glob(value)) + match_type = MATCH_TYPE_GLOB; + else + match_type = MATCH_TYPE_PLAIN; + + if (type_has_nulstr_value(type)) { + /* Convert value string to nulstr. */ + bool bar = true, empty = false; + char *a, *b; + + for (a = b = value; *a != '\0'; a++) { + if (*a != '|') { + *b++ = *a; + bar = false; + } else { + if (bar) + empty = true; + else + *b++ = '\0'; + bar = true; + } + } + *b = '\0'; + + /* Make sure the value is end, so NULSTR_FOREACH can read correct match */ + if (b < a) + b[1] = '\0'; + + if (bar) + empty = true; + + if (empty) { + if (match_type == MATCH_TYPE_GLOB) + match_type = MATCH_TYPE_GLOB_WITH_EMPTY; + if (match_type == MATCH_TYPE_PLAIN) + match_type = MATCH_TYPE_PLAIN_WITH_EMPTY; + } + } + } + + if (IN_SET(type, TK_M_ATTR, TK_M_PARENTS_ATTR)) { + assert(value); + assert(data); + + len = strlen(value); + if (len > 0 && !isspace(value[len - 1])) + remove_trailing_whitespace = true; + + subst_type = rule_get_substitution_type(data); + } + + token = new(UdevRuleToken, 1); + if (!token) + return -ENOMEM; + + *token = (UdevRuleToken) { + .type = type, + .op = op, + .value = value, + .data = data, + .match_type = match_type, + .attr_subst_type = subst_type, + .attr_match_remove_trailing_whitespace = remove_trailing_whitespace, + .rule_line = rule_line, + }; + + LIST_APPEND(tokens, rule_line->tokens, token); + + if (token->type == TK_A_NAME) + SET_FLAG(rule_line->type, LINE_HAS_NAME, true); + + else if (IN_SET(token->type, TK_A_DEVLINK, + TK_A_OWNER, TK_A_GROUP, TK_A_MODE, + TK_A_OWNER_ID, TK_A_GROUP_ID, TK_A_MODE_ID)) + SET_FLAG(rule_line->type, LINE_HAS_DEVLINK, true); + + else if (token->type == TK_A_OPTIONS_STATIC_NODE) + SET_FLAG(rule_line->type, LINE_HAS_STATIC_NODE, true); + + else if (token->type >= _TK_A_MIN || + IN_SET(token->type, TK_M_PROGRAM, + TK_M_IMPORT_FILE, TK_M_IMPORT_PROGRAM, TK_M_IMPORT_BUILTIN, + TK_M_IMPORT_DB, TK_M_IMPORT_CMDLINE, TK_M_IMPORT_PARENT)) + SET_FLAG(rule_line->type, LINE_UPDATE_SOMETHING, true); + + TAKE_PTR(token); + return 0; +} + +static void check_value_format_and_warn(UdevRuleLine *line, const char *key, const char *value, bool nonempty) { + size_t offset; + const char *hint; + + if (nonempty && isempty(value)) + log_line_invalid_value(line, key, value, (size_t) 0, "empty value"); + else if (udev_check_format(value, &offset, &hint) < 0) + log_line_invalid_value(line, key, value, offset + 1, hint); +} + +static int check_attr_format_and_warn(UdevRuleLine *line, const char *key, const char *value) { + size_t offset; + const char *hint; + + if (isempty(value)) + return log_line_invalid_attr(line, key); + if (udev_check_format(value, &offset, &hint) < 0) + log_line_invalid_attr_format(line, key, value, offset + 1, hint); + return 0; +} + +static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, UdevRuleOperatorType op, char *value) { + ResolveNameTiming resolve_name_timing = LINE_GET_RULES(rule_line)->resolve_name_timing; + bool is_match = IN_SET(op, OP_MATCH, OP_NOMATCH); + int r; + + assert(key); + assert(value); + + if (streq(key, "ACTION")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_ACTION, op, value, NULL); + } else if (streq(key, "DEVPATH")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_DEVPATH, op, value, NULL); + } else if (streq(key, "KERNEL")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_KERNEL, op, value, NULL); + } else if (streq(key, "SYMLINK")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) { + check_value_format_and_warn(rule_line, key, value, false); + r = rule_line_add_token(rule_line, TK_A_DEVLINK, op, value, NULL); + } else + r = rule_line_add_token(rule_line, TK_M_DEVLINK, op, value, NULL); + } else if (streq(key, "NAME")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ADD) { + log_line_warning(rule_line, "%s key takes '==', '!=', '=', or ':=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + if (streq(value, "%k")) + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), + "Ignoring NAME=\"%%k\", as it will take no effect."); + if (isempty(value)) + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), + "Ignoring NAME=\"\", as udev will not delete any network interfaces."); + check_value_format_and_warn(rule_line, key, value, false); + + r = rule_line_add_token(rule_line, TK_A_NAME, op, value, NULL); + } else + r = rule_line_add_token(rule_line, TK_M_NAME, op, value, NULL); + } else if (streq(key, "ENV")) { + if (isempty(attr)) + return log_line_invalid_attr(rule_line, key); + if (op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ASSIGN_FINAL) { + log_line_warning(rule_line, "%s key takes '==', '!=', '=', or '+=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + if (STR_IN_SET(attr, + "ACTION", "DEVLINKS", "DEVNAME", "DEVPATH", "DEVTYPE", "DRIVER", + "IFINDEX", "MAJOR", "MINOR", "SEQNUM", "SUBSYSTEM", "TAGS")) + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), + "Invalid ENV attribute. '%s' cannot be set.", attr); + + check_value_format_and_warn(rule_line, key, value, false); + + r = rule_line_add_token(rule_line, TK_A_ENV, op, value, attr); + } else + r = rule_line_add_token(rule_line, TK_M_ENV, op, value, attr); + } else if (streq(key, "CONST")) { + if (isempty(attr) || !STR_IN_SET(attr, "arch", "virt")) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + r = rule_line_add_token(rule_line, TK_M_CONST, op, value, attr); + } else if (streq(key, "TAG")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (op == OP_ASSIGN_FINAL) { + log_line_warning(rule_line, "%s key takes '==', '!=', '=', or '+=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + check_value_format_and_warn(rule_line, key, value, true); + + r = rule_line_add_token(rule_line, TK_A_TAG, op, value, NULL); + } else + r = rule_line_add_token(rule_line, TK_M_TAG, op, value, NULL); + } else if (streq(key, "SUBSYSTEM")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + if (STR_IN_SET(value, "bus", "class")) + log_line_warning(rule_line, "\"%s\" must be specified as \"subsystem\".", value); + + r = rule_line_add_token(rule_line, TK_M_SUBSYSTEM, op, value, NULL); + } else if (streq(key, "DRIVER")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_DRIVER, op, value, NULL); + } else if (streq(key, "ATTR")) { + r = check_attr_format_and_warn(rule_line, key, attr); + if (r < 0) + return r; + if (op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) { + log_line_warning(rule_line, "%s key takes '==', '!=', or '=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + check_value_format_and_warn(rule_line, key, value, false); + r = rule_line_add_token(rule_line, TK_A_ATTR, op, value, attr); + } else + r = rule_line_add_token(rule_line, TK_M_ATTR, op, value, attr); + } else if (streq(key, "SYSCTL")) { + r = check_attr_format_and_warn(rule_line, key, attr); + if (r < 0) + return r; + if (op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) { + log_line_warning(rule_line, "%s key takes '==', '!=', or '=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + check_value_format_and_warn(rule_line, key, value, false); + r = rule_line_add_token(rule_line, TK_A_SYSCTL, op, value, attr); + } else + r = rule_line_add_token(rule_line, TK_M_SYSCTL, op, value, attr); + } else if (streq(key, "KERNELS")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_KERNEL, op, value, NULL); + } else if (streq(key, "SUBSYSTEMS")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_SUBSYSTEM, op, value, NULL); + } else if (streq(key, "DRIVERS")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_DRIVER, op, value, NULL); + } else if (streq(key, "ATTRS")) { + r = check_attr_format_and_warn(rule_line, key, attr); + if (r < 0) + return r; + if (!is_match) + return log_line_invalid_op(rule_line, key); + + if (startswith(attr, "device/")) + log_line_warning(rule_line, "'device' link may not be available in future kernels."); + if (strstr(attr, "../")) + log_line_warning(rule_line, "Direct reference to parent sysfs directory, may break in future kernels."); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_ATTR, op, value, attr); + } else if (streq(key, "TAGS")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_TAG, op, value, NULL); + } else if (streq(key, "TEST")) { + mode_t mode = MODE_INVALID; + + if (!isempty(attr)) { + r = parse_mode(attr, &mode); + if (r < 0) + return log_line_error_errno(rule_line, r, "Failed to parse mode '%s': %m", attr); + } + check_value_format_and_warn(rule_line, key, value, true); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_TEST, op, value, MODE_TO_PTR(mode)); + } else if (streq(key, "PROGRAM")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + check_value_format_and_warn(rule_line, key, value, true); + if (op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (!is_match) + op = OP_MATCH; + + r = rule_line_add_token(rule_line, TK_M_PROGRAM, op, value, NULL); + } else if (streq(key, "IMPORT")) { + if (isempty(attr)) + return log_line_invalid_attr(rule_line, key); + check_value_format_and_warn(rule_line, key, value, true); + if (op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (!is_match) + op = OP_MATCH; + + if (streq(attr, "file")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_FILE, op, value, NULL); + else if (streq(attr, "program")) { + UdevBuiltinCommand cmd; + + cmd = udev_builtin_lookup(value); + if (cmd >= 0) { + log_line_debug(rule_line, "Found builtin command '%s' for %s, replacing attribute.", value, key); + r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + } else + r = rule_line_add_token(rule_line, TK_M_IMPORT_PROGRAM, op, value, NULL); + } else if (streq(attr, "builtin")) { + UdevBuiltinCommand cmd; + + cmd = udev_builtin_lookup(value); + if (cmd < 0) + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), + "Unknown builtin command: %s", value); + r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + } else if (streq(attr, "db")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_DB, op, value, NULL); + else if (streq(attr, "cmdline")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_CMDLINE, op, value, NULL); + else if (streq(attr, "parent")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_PARENT, op, value, NULL); + else + return log_line_invalid_attr(rule_line, key); + } else if (streq(key, "RESULT")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (!is_match) + return log_line_invalid_op(rule_line, key); + + r = rule_line_add_token(rule_line, TK_M_RESULT, op, value, NULL); + } else if (streq(key, "OPTIONS")) { + char *tmp; + + if (attr) + return log_line_invalid_attr(rule_line, key); + if (is_match || op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ADD) + op = OP_ASSIGN; + + if (streq(value, "string_escape=none")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_NONE, op, NULL, NULL); + else if (streq(value, "string_escape=replace")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_REPLACE, op, NULL, NULL); + else if (streq(value, "db_persist")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_DB_PERSIST, op, NULL, NULL); + else if (streq(value, "watch")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(1)); + else if (streq(value, "nowatch")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(0)); + else if ((tmp = startswith(value, "static_node="))) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STATIC_NODE, op, tmp, NULL); + else if ((tmp = startswith(value, "link_priority="))) { + int prio; + + r = safe_atoi(tmp, &prio); + if (r < 0) + return log_line_error_errno(rule_line, r, "Failed to parse link priority '%s': %m", tmp); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_DEVLINK_PRIORITY, op, NULL, INT_TO_PTR(prio)); + } else if ((tmp = startswith(value, "log_level="))) { + int level; + + if (streq(tmp, "reset")) + level = -1; + else { + level = log_level_from_string(tmp); + if (level < 0) + return log_line_error_errno(rule_line, level, "Failed to parse log level '%s': %m", tmp); + } + r = rule_line_add_token(rule_line, TK_A_OPTIONS_LOG_LEVEL, op, NULL, INT_TO_PTR(level)); + } else { + log_line_warning(rule_line, "Invalid value for OPTIONS key, ignoring: '%s'", value); + return 0; + } + } else if (streq(key, "OWNER")) { + uid_t uid; + + if (attr) + return log_line_invalid_attr(rule_line, key); + if (is_match || op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ADD) { + log_line_warning(rule_line, "%s key takes '=' or ':=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (parse_uid(value, &uid) >= 0) + r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid)); + else if (resolve_name_timing == RESOLVE_NAME_EARLY && + rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) { + r = rule_resolve_user(rule_line, value, &uid); + if (r < 0) + return log_line_error_errno(rule_line, r, "Failed to resolve user name '%s': %m", value); + + r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid)); + } else if (resolve_name_timing != RESOLVE_NAME_NEVER) { + check_value_format_and_warn(rule_line, key, value, true); + r = rule_line_add_token(rule_line, TK_A_OWNER, op, value, NULL); + } else { + log_line_debug(rule_line, "User name resolution is disabled, ignoring %s=\"%s\".", key, value); + return 0; + } + } else if (streq(key, "GROUP")) { + gid_t gid; + + if (attr) + return log_line_invalid_attr(rule_line, key); + if (is_match || op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ADD) { + log_line_warning(rule_line, "%s key takes '=' or ':=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (parse_gid(value, &gid) >= 0) + r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid)); + else if (resolve_name_timing == RESOLVE_NAME_EARLY && + rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) { + r = rule_resolve_group(rule_line, value, &gid); + if (r < 0) + return log_line_error_errno(rule_line, r, "Failed to resolve group name '%s': %m", value); + + r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid)); + } else if (resolve_name_timing != RESOLVE_NAME_NEVER) { + check_value_format_and_warn(rule_line, key, value, true); + r = rule_line_add_token(rule_line, TK_A_GROUP, op, value, NULL); + } else { + log_line_debug(rule_line, "Resolving group name is disabled, ignoring GROUP=\"%s\".", value); + return 0; + } + } else if (streq(key, "MODE")) { + mode_t mode; + + if (attr) + return log_line_invalid_attr(rule_line, key); + if (is_match || op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ADD) { + log_line_warning(rule_line, "%s key takes '=' or ':=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + if (parse_mode(value, &mode) >= 0) + r = rule_line_add_token(rule_line, TK_A_MODE_ID, op, NULL, MODE_TO_PTR(mode)); + else { + check_value_format_and_warn(rule_line, key, value, true); + r = rule_line_add_token(rule_line, TK_A_MODE, op, value, NULL); + } + } else if (streq(key, "SECLABEL")) { + if (isempty(attr)) + return log_line_invalid_attr(rule_line, key); + check_value_format_and_warn(rule_line, key, value, true); + if (is_match || op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + if (op == OP_ASSIGN_FINAL) { + log_line_warning(rule_line, "%s key takes '=' or '+=' operator, assuming '='.", key); + op = OP_ASSIGN; + } + + r = rule_line_add_token(rule_line, TK_A_SECLABEL, op, value, attr); + } else if (streq(key, "RUN")) { + if (is_match || op == OP_REMOVE) + return log_line_invalid_op(rule_line, key); + check_value_format_and_warn(rule_line, key, value, true); + if (!attr || streq(attr, "program")) + r = rule_line_add_token(rule_line, TK_A_RUN_PROGRAM, op, value, NULL); + else if (streq(attr, "builtin")) { + UdevBuiltinCommand cmd; + + cmd = udev_builtin_lookup(value); + if (cmd < 0) + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), + "Unknown builtin command '%s', ignoring.", value); + r = rule_line_add_token(rule_line, TK_A_RUN_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + } else + return log_line_invalid_attr(rule_line, key); + } else if (streq(key, "GOTO")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (op != OP_ASSIGN) + return log_line_invalid_op(rule_line, key); + if (FLAGS_SET(rule_line->type, LINE_HAS_GOTO)) { + log_line_warning(rule_line, "Contains multiple GOTO keys, ignoring GOTO=\"%s\".", value); + return 0; + } + + rule_line->goto_label = value; + SET_FLAG(rule_line->type, LINE_HAS_GOTO, true); + return 1; + } else if (streq(key, "LABEL")) { + if (attr) + return log_line_invalid_attr(rule_line, key); + if (op != OP_ASSIGN) + return log_line_invalid_op(rule_line, key); + if (FLAGS_SET(rule_line->type, LINE_HAS_LABEL)) + log_line_warning(rule_line, "Contains multiple LABEL keys, ignoring LABEL=\"%s\".", + rule_line->label); + + rule_line->label = value; + SET_FLAG(rule_line->type, LINE_HAS_LABEL, true); + return 1; + } else + return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), "Invalid key '%s'.", key); + if (r < 0) + return log_oom(); + + return 1; +} + +static UdevRuleOperatorType parse_operator(const char *op) { + assert(op); + + if (startswith(op, "==")) + return OP_MATCH; + if (startswith(op, "!=")) + return OP_NOMATCH; + if (startswith(op, "+=")) + return OP_ADD; + if (startswith(op, "-=")) + return OP_REMOVE; + if (startswith(op, "=")) + return OP_ASSIGN; + if (startswith(op, ":=")) + return OP_ASSIGN_FINAL; + + return _OP_TYPE_INVALID; +} + +static void check_token_delimiters(UdevRuleLine *rule_line, const char *line) { + assert(rule_line); + + size_t n_comma = 0; + bool ws_before_comma = false, ws_after_comma = false; + const char *p; + + for (p = line; !isempty(p); ++p) { + if (*p == ',') + ++n_comma; + else if (strchr(WHITESPACE, *p)) { + if (n_comma > 0) + ws_after_comma = true; + else + ws_before_comma = true; + } else + break; + } + + if (line == rule_line->line) { + /* this is the first token of the rule */ + if (n_comma > 0) + log_line_notice(rule_line, "style: stray leading comma."); + } else if (isempty(p)) { + /* there are no more tokens in the rule */ + if (n_comma > 0) + log_line_notice(rule_line, "style: stray trailing comma."); + } else { + /* single comma is expected */ + if (n_comma == 0) + log_line_notice(rule_line, "style: a comma between tokens is expected."); + else if (n_comma > 1) + log_line_notice(rule_line, "style: more than one comma between tokens."); + + /* whitespace after comma is expected */ + if (n_comma > 0) { + if (ws_before_comma) + log_line_notice(rule_line, "style: stray whitespace before comma."); + if (!ws_after_comma) + log_line_notice(rule_line, "style: whitespace after comma is expected."); + } else if (!ws_before_comma && !ws_after_comma) + log_line_notice(rule_line, "style: whitespace between tokens is expected."); + } +} + +int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos) { + char *i, *j; + bool is_escaped; + + /* value must be double quotated */ + is_escaped = str[0] == 'e'; + str += is_escaped; + if (str[0] != '"') + return -EINVAL; + + if (!is_escaped) { + /* unescape double quotation '\"'->'"' */ + for (j = str, i = str + 1; *i != '"'; i++, j++) { + if (*i == '\0') + return -EINVAL; + if (i[0] == '\\' && i[1] == '"') + i++; + *j = *i; + } + j[0] = '\0'; + /* + * The return value must be terminated by two subsequent NULs + * so it could be safely interpreted as nulstr. + */ + j[1] = '\0'; + } else { + _cleanup_free_ char *unescaped = NULL; + ssize_t l; + + /* find the end position of value */ + for (i = str + 1; *i != '"'; i++) { + if (i[0] == '\\') + i++; + if (*i == '\0') + return -EINVAL; + } + i[0] = '\0'; + + l = cunescape_length(str + 1, i - (str + 1), 0, &unescaped); + if (l < 0) + return l; + + assert(l <= i - (str + 1)); + memcpy(str, unescaped, l + 1); + /* + * The return value must be terminated by two subsequent NULs + * so it could be safely interpreted as nulstr. + */ + str[l + 1] = '\0'; + } + + *ret_value = str; + *ret_endpos = i + 1; + return 0; +} + +static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOperatorType *ret_op, char **ret_value) { + char *key_begin, *key_end, *attr, *tmp; + UdevRuleOperatorType op; + int r; + + assert(line); + assert(*line); + assert(ret_key); + assert(ret_op); + assert(ret_value); + + key_begin = skip_leading_chars(*line, WHITESPACE ","); + + if (isempty(key_begin)) + return 0; + + for (key_end = key_begin; ; key_end++) { + if (key_end[0] == '\0') + return -EINVAL; + if (strchr(WHITESPACE "={", key_end[0])) + break; + if (strchr("+-!:", key_end[0]) && key_end[1] == '=') + break; + } + if (key_end[0] == '{') { + attr = key_end + 1; + tmp = strchr(attr, '}'); + if (!tmp) + return -EINVAL; + *tmp++ = '\0'; + } else { + attr = NULL; + tmp = key_end; + } + + tmp = skip_leading_chars(tmp, NULL); + op = parse_operator(tmp); + if (op < 0) + return -EINVAL; + + key_end[0] = '\0'; + + tmp += op == OP_ASSIGN ? 1 : 2; + tmp = skip_leading_chars(tmp, NULL); + r = udev_rule_parse_value(tmp, ret_value, line); + if (r < 0) + return r; + + *ret_key = key_begin; + *ret_attr = attr; + *ret_op = op; + return 1; +} + +static void check_tokens_order(UdevRuleLine *rule_line) { + bool has_result = false; + + assert(rule_line); + + LIST_FOREACH(tokens, t, rule_line->tokens) + if (t->type == TK_M_RESULT) + has_result = true; + else if (has_result && t->type == TK_M_PROGRAM) { + log_line_warning(rule_line, "Reordering RESULT check after PROGRAM assignment."); + break; + } +} + +static void sort_tokens(UdevRuleLine *rule_line) { + assert(rule_line); + + UdevRuleToken *old_tokens = TAKE_PTR(rule_line->tokens); + + while (old_tokens) { + UdevRuleToken *min_token = NULL; + + LIST_FOREACH(tokens, t, old_tokens) + if (!min_token || min_token->type > t->type) + min_token = t; + + LIST_REMOVE(tokens, old_tokens, min_token); + LIST_APPEND(tokens, rule_line->tokens, min_token); + } +} + +static int rule_add_line(UdevRuleFile *rule_file, const char *line_str, unsigned line_nr, bool extra_checks) { + _cleanup_(udev_rule_line_freep) UdevRuleLine *rule_line = NULL; + _cleanup_free_ char *line = NULL; + char *p; + int r; + + assert(rule_file); + assert(line_str); + + if (isempty(line_str)) + return 0; + + line = strdup(line_str); + if (!line) + return log_oom(); + + rule_line = new(UdevRuleLine, 1); + if (!rule_line) + return log_oom(); + + *rule_line = (UdevRuleLine) { + .line = TAKE_PTR(line), + .line_number = line_nr, + .rule_file = rule_file, + }; + + LIST_APPEND(rule_lines, rule_file->rule_lines, rule_line); + + for (p = rule_line->line; !isempty(p); ) { + char *key, *attr, *value; + UdevRuleOperatorType op; + + if (extra_checks) + check_token_delimiters(rule_line, p); + + r = parse_line(&p, &key, &attr, &op, &value); + if (r < 0) + return log_line_error_errno(rule_line, r, "Invalid key/value pair, ignoring."); + if (r == 0) + break; + + r = parse_token(rule_line, key, attr, op, value); + if (r < 0) + return r; + } + + if (rule_line->type == 0) { + log_line_warning(rule_line, "The line has no effect, ignoring."); + return 0; + } + + if (extra_checks) + check_tokens_order(rule_line); + + sort_tokens(rule_line); + TAKE_PTR(rule_line); + return 0; +} + +static void rule_resolve_goto(UdevRuleFile *rule_file) { + assert(rule_file); + + /* link GOTOs to LABEL rules in this file to be able to fast-forward */ + LIST_FOREACH(rule_lines, line, rule_file->rule_lines) { + if (!FLAGS_SET(line->type, LINE_HAS_GOTO)) + continue; + + LIST_FOREACH(rule_lines, i, line->rule_lines_next) + if (streq_ptr(i->label, line->goto_label)) { + line->goto_line = i; + SET_FLAG(i->type, LINE_IS_REFERENCED, true); + break; + } + + if (!line->goto_line) { + log_line_error(line, "GOTO=\"%s\" has no matching label, ignoring.", + line->goto_label); + + SET_FLAG(line->type, LINE_HAS_GOTO, false); + line->goto_label = NULL; + + if ((line->type & ~(LINE_HAS_LABEL|LINE_IS_REFERENCED)) == 0) { + log_line_warning(line, "The line has no effect any more, dropping."); + /* LINE_IS_REFERENCED implies LINE_HAS_LABEL */ + if (line->type & LINE_HAS_LABEL) + udev_rule_line_clear_tokens(line); + else + udev_rule_line_free(line); + } + } + } +} + +static bool token_data_is_string(UdevRuleTokenType type) { + return IN_SET(type, TK_M_ENV, + TK_M_CONST, + TK_M_ATTR, + TK_M_SYSCTL, + TK_M_PARENTS_ATTR, + TK_A_SECLABEL, + TK_A_ENV, + TK_A_ATTR, + TK_A_SYSCTL); +} + +static bool token_type_and_data_eq(const UdevRuleToken *a, const UdevRuleToken *b) { + assert(a); + assert(b); + + return a->type == b->type && + (token_data_is_string(a->type) ? streq_ptr(a->data, b->data) : (a->data == b->data)); +} + +static bool nulstr_eq(const char *a, const char *b) { + NULSTR_FOREACH(i, a) + if (!nulstr_contains(b, i)) + return false; + + NULSTR_FOREACH(i, b) + if (!nulstr_contains(a, i)) + return false; + + return true; +} + +static bool token_type_and_value_eq(const UdevRuleToken *a, const UdevRuleToken *b) { + assert(a); + assert(b); + + if (a->type != b->type || + a->match_type != b->match_type) + return false; + + /* token value is ignored for certain match types */ + if (IN_SET(a->match_type, MATCH_TYPE_EMPTY, MATCH_TYPE_SUBSYSTEM)) + return true; + + return type_has_nulstr_value(a->type) ? nulstr_eq(a->value, b->value) : + streq_ptr(a->value, b->value); +} + +static bool conflicting_op(UdevRuleOperatorType a, UdevRuleOperatorType b) { + return (a == OP_MATCH && b == OP_NOMATCH) || + (a == OP_NOMATCH && b == OP_MATCH); +} + +/* test whether all fields besides UdevRuleOperatorType of two tokens match */ +static bool tokens_eq(const UdevRuleToken *a, const UdevRuleToken *b) { + assert(a); + assert(b); + + return a->attr_subst_type == b->attr_subst_type && + a->attr_match_remove_trailing_whitespace == b->attr_match_remove_trailing_whitespace && + token_type_and_value_eq(a, b) && + token_type_and_data_eq(a, b); +} + +static bool nulstr_tokens_conflict(const UdevRuleToken *a, const UdevRuleToken *b) { + assert(a); + assert(b); + + if (!(a->type == b->type && + type_has_nulstr_value(a->type) && + a->op == b->op && + a->op == OP_MATCH && + a->match_type == b->match_type && + a->attr_subst_type == b->attr_subst_type && + a->attr_match_remove_trailing_whitespace == b->attr_match_remove_trailing_whitespace && + token_type_and_data_eq(a, b))) + return false; + + if (a->match_type == MATCH_TYPE_PLAIN) { + NULSTR_FOREACH(i, a->value) + if (nulstr_contains(b->value, i)) + return false; + return true; + } + + if (a->match_type == MATCH_TYPE_GLOB) { + NULSTR_FOREACH(i, a->value) { + size_t i_n = strcspn(i, GLOB_CHARS); + if (i_n == 0) + return false; + NULSTR_FOREACH(j, b->value) { + size_t j_n = strcspn(j, GLOB_CHARS); + if (j_n == 0 || strneq(i, j, MIN(i_n, j_n))) + return false; + } + + } + return true; + } + + return false; +} + +static void udev_check_unused_labels(UdevRuleLine *line) { + assert(line); + + if (FLAGS_SET(line->type, LINE_HAS_LABEL) && + !FLAGS_SET(line->type, LINE_IS_REFERENCED)) + log_line_notice(line, "style: LABEL=\"%s\" is unused.", line->label); +} + +static void udev_check_conflicts_duplicates(UdevRuleLine *line) { + assert(line); + + bool conflicts = false, duplicates = false; + + LIST_FOREACH(tokens, token, line->tokens) + LIST_FOREACH(tokens, i, token->tokens_next) { + bool new_conflicts = false, new_duplicates = false; + + if (tokens_eq(token, i)) { + if (!duplicates && token->op == i->op) + new_duplicates = true; + if (!conflicts && conflicting_op(token->op, i->op)) + new_conflicts = true; + } else if (!conflicts && nulstr_tokens_conflict(token, i)) + new_conflicts = true; + else + continue; + + if (new_duplicates) { + duplicates = new_duplicates; + log_line_warning(line, "duplicate expressions."); + } + if (new_conflicts) { + conflicts = new_conflicts; + log_line_error(line, "conflicting match expressions, the line has no effect."); + } + if (conflicts && duplicates) + return; + } +} + +static void udev_check_rule_line(UdevRuleLine *line) { + udev_check_unused_labels(line); + udev_check_conflicts_duplicates(line); +} + +int udev_rules_parse_file(UdevRules *rules, const char *filename, bool extra_checks, UdevRuleFile **ret) { + _cleanup_(udev_rule_file_freep) UdevRuleFile *rule_file = NULL; + _cleanup_free_ char *continuation = NULL, *name = NULL; + _cleanup_fclose_ FILE *f = NULL; + bool ignore_line = false; + unsigned line_nr = 0; + struct stat st; + int r; + + assert(rules); + assert(filename); + + f = fopen(filename, "re"); + if (!f) { + if (extra_checks) + return -errno; + + if (errno == ENOENT) + return 0; + + return log_warning_errno(errno, "Failed to open %s, ignoring: %m", filename); + } + + if (fstat(fileno(f), &st) < 0) + return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", filename); + + if (null_or_empty(&st)) { + log_debug("Skipping empty file: %s", filename); + if (ret) + *ret = NULL; + return 0; + } + + r = hashmap_put_stats_by_path(&rules->stats_by_path, filename, &st); + if (r < 0) + return log_warning_errno(errno, "Failed to save stat for %s, ignoring: %m", filename); + + (void) fd_warn_permissions(filename, fileno(f)); + + log_debug("Reading rules file: %s", filename); + + name = strdup(filename); + if (!name) + return log_oom(); + + rule_file = new(UdevRuleFile, 1); + if (!rule_file) + return log_oom(); + + *rule_file = (UdevRuleFile) { + .filename = TAKE_PTR(name), + .rules = rules, + }; + + LIST_APPEND(rule_files, rules->rule_files, rule_file); + + for (;;) { + _cleanup_free_ char *buf = NULL; + size_t len; + char *line; + + r = read_line(f, UDEV_LINE_SIZE, &buf); + if (r < 0) + return r; + if (r == 0) + break; + + line_nr++; + line = skip_leading_chars(buf, NULL); + + /* Lines beginning with '#' are ignored regardless of line continuation. */ + if (line[0] == '#') + continue; + + len = strlen(line); + + if (continuation && !ignore_line) { + if (strlen(continuation) + len >= UDEV_LINE_SIZE) + ignore_line = true; + + if (!strextend(&continuation, line)) + return log_oom(); + + if (!ignore_line) { + line = continuation; + len = strlen(line); + } + } + + if (len > 0 && line[len - 1] == '\\') { + if (ignore_line) + continue; + + line[len - 1] = '\0'; + if (!continuation) { + continuation = strdup(line); + if (!continuation) + return log_oom(); + } + + continue; + } + + if (ignore_line) + log_file_error(rule_file, line_nr, "Line is too long, ignored."); + else if (len > 0) + (void) rule_add_line(rule_file, line, line_nr, extra_checks); + + continuation = mfree(continuation); + ignore_line = false; + } + + if (continuation) + log_file_error(rule_file, line_nr, + "Unexpected EOF after line continuation, line ignored."); + + rule_resolve_goto(rule_file); + + if (extra_checks) + LIST_FOREACH(rule_lines, line, rule_file->rule_lines) + udev_check_rule_line(line); + + if (ret) + *ret = rule_file; + + TAKE_PTR(rule_file); + return 1; +} + +unsigned udev_rule_file_get_issues(UdevRuleFile *rule_file) { + assert(rule_file); + + return rule_file->issues; +} + +UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing) { + assert(resolve_name_timing >= 0 && resolve_name_timing < _RESOLVE_NAME_TIMING_MAX); + + UdevRules *rules = new(UdevRules, 1); + if (!rules) + return NULL; + + *rules = (UdevRules) { + .resolve_name_timing = resolve_name_timing, + }; + + return rules; +} + +int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_strv_free_ char **files = NULL; + int r; + + rules = udev_rules_new(resolve_name_timing); + if (!rules) + return -ENOMEM; + + r = conf_files_list_strv(&files, ".rules", NULL, 0, RULES_DIRS); + if (r < 0) + return log_debug_errno(r, "Failed to enumerate rules files: %m"); + + STRV_FOREACH(f, files) { + r = udev_rules_parse_file(rules, *f, /* extra_checks = */ false, NULL); + if (r < 0) + log_debug_errno(r, "Failed to read rules file %s, ignoring: %m", *f); + } + + *ret_rules = TAKE_PTR(rules); + return 0; +} + +bool udev_rules_should_reload(UdevRules *rules) { + _cleanup_hashmap_free_ Hashmap *stats_by_path = NULL; + int r; + + if (!rules) + return true; + + r = config_get_stats_by_path(".rules", NULL, 0, RULES_DIRS, /* check_dropins = */ false, &stats_by_path); + if (r < 0) { + log_warning_errno(r, "Failed to get stats of udev rules, ignoring: %m"); + return true; + } + + if (!stats_by_path_equal(rules->stats_by_path, stats_by_path)) { + log_debug("Udev rules need reloading"); + return true; + } + + return false; +} + +static bool token_match_string(UdevRuleToken *token, const char *str) { + const char *value; + bool match = false; + + assert(token); + assert(token->value); + assert(token->type < _TK_M_MAX); + + str = strempty(str); + value = token->value; + + switch (token->match_type) { + case MATCH_TYPE_EMPTY: + match = isempty(str); + break; + case MATCH_TYPE_SUBSYSTEM: + match = STR_IN_SET(str, "subsystem", "class", "bus"); + break; + case MATCH_TYPE_PLAIN_WITH_EMPTY: + if (isempty(str)) { + match = true; + break; + } + _fallthrough_; + case MATCH_TYPE_PLAIN: + NULSTR_FOREACH(i, value) + if (streq(i, str)) { + match = true; + break; + } + break; + case MATCH_TYPE_GLOB_WITH_EMPTY: + if (isempty(str)) { + match = true; + break; + } + _fallthrough_; + case MATCH_TYPE_GLOB: + NULSTR_FOREACH(i, value) + if ((fnmatch(i, str, 0) == 0)) { + match = true; + break; + } + break; + default: + assert_not_reached(); + } + + return token->op == (match ? OP_MATCH : OP_NOMATCH); +} + +static bool token_match_attr(UdevRuleToken *token, sd_device *dev, UdevEvent *event) { + char nbuf[UDEV_NAME_SIZE], vbuf[UDEV_NAME_SIZE]; + const char *name, *value; + bool truncated; + + assert(token); + assert(IN_SET(token->type, TK_M_ATTR, TK_M_PARENTS_ATTR)); + assert(dev); + assert(event); + + name = token->data; + + switch (token->attr_subst_type) { + case SUBST_TYPE_FORMAT: + (void) udev_event_apply_format(event, name, nbuf, sizeof(nbuf), false, NULL, &truncated); + if (truncated) { + log_event_truncated(dev, token, "sysfs attribute name", name, + token->type == TK_M_ATTR ? "ATTR" : "ATTRS", /* is_match = */ true); + return false; + } + + name = nbuf; + _fallthrough_; + case SUBST_TYPE_PLAIN: + if (sd_device_get_sysattr_value(dev, name, &value) < 0) + return false; + break; + case SUBST_TYPE_SUBSYS: + if (udev_resolve_subsys_kernel(name, vbuf, sizeof(vbuf), true) < 0) + return false; + value = vbuf; + break; + default: + assert_not_reached(); + } + + /* remove trailing whitespace, if not asked to match for it */ + if (token->attr_match_remove_trailing_whitespace) { + if (value != vbuf) { + strscpy(vbuf, sizeof(vbuf), value); + value = vbuf; + } + + delete_trailing_chars(vbuf, NULL); + } + + return token_match_string(token, value); +} + +static int get_property_from_string(char *line, char **ret_key, char **ret_value) { + char *key, *val; + size_t len; + + assert(line); + assert(ret_key); + assert(ret_value); + + /* find key */ + key = skip_leading_chars(line, NULL); + + /* comment or empty line */ + if (IN_SET(key[0], '#', '\0')) { + *ret_key = *ret_value = NULL; + return 0; + } + + /* split key/value */ + val = strchr(key, '='); + if (!val) + return -EINVAL; + *val++ = '\0'; + + key = strstrip(key); + if (isempty(key)) + return -EINVAL; + + val = strstrip(val); + if (isempty(val)) + return -EINVAL; + + /* unquote */ + if (IN_SET(val[0], '"', '\'')) { + len = strlen(val); + if (len == 1 || val[len-1] != val[0]) + return -EINVAL; + val[len-1] = '\0'; + val++; + } + + *ret_key = key; + *ret_value = val; + return 1; +} + +static int import_parent_into_properties(sd_device *dev, const char *filter) { + sd_device *parent; + int r; + + assert(dev); + assert(filter); + + r = sd_device_get_parent(dev, &parent); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + FOREACH_DEVICE_PROPERTY(parent, key, val) { + if (fnmatch(filter, key, 0) != 0) + continue; + r = device_add_property(dev, key, val); + if (r < 0) + return r; + } + + return 1; +} + +static int attr_subst_subdir(char attr[static UDEV_PATH_SIZE]) { + _cleanup_closedir_ DIR *dir = NULL; + char buf[UDEV_PATH_SIZE], *p; + const char *tail; + size_t len, size; + bool truncated; + + assert(attr); + + tail = strstr(attr, "/*/"); + if (!tail) + return 0; + + len = tail - attr + 1; /* include slash at the end */ + tail += 2; /* include slash at the beginning */ + + p = buf; + size = sizeof(buf); + size -= strnpcpy_full(&p, size, attr, len, &truncated); + if (truncated) + return -ENOENT; + + dir = opendir(buf); + if (!dir) + return -errno; + + FOREACH_DIRENT_ALL(de, dir, break) { + if (de->d_name[0] == '.') + continue; + + strscpyl_full(p, size, &truncated, de->d_name, tail, NULL); + if (truncated) + continue; + + if (faccessat(dirfd(dir), p, F_OK, 0) < 0) + continue; + + strcpy(attr, buf); + return 0; + } + + return -ENOENT; +} + +static size_t udev_replace_ifname(char *str) { + size_t replaced = 0; + + assert(str); + + /* See ifname_valid_full(). */ + + for (char *p = str; *p != '\0'; p++) + if (!ifname_valid_char(*p)) { + *p = '_'; + replaced++; + } + + return replaced; +} + +static int udev_rule_apply_token_to_event( + UdevRuleToken *token, + sd_device *dev, + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list) { + + int r; + + assert(token); + assert(dev); + assert(event); + + /* This returns the following values: + * 0 on the current token does not match the event, + * 1 on the current token matches the event, and + * negative errno on some critical errors. */ + + switch (token->type) { + case TK_M_ACTION: { + sd_device_action_t a; + + r = sd_device_get_action(dev, &a); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to get uevent action type: %m"); + + return token_match_string(token, device_action_to_string(a)); + } + case TK_M_DEVPATH: { + const char *val; + + r = sd_device_get_devpath(dev, &val); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to get devpath: %m"); + + return token_match_string(token, val); + } + case TK_M_KERNEL: + case TK_M_PARENTS_KERNEL: { + const char *val; + + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to get sysname: %m"); + + return token_match_string(token, val); + } + case TK_M_DEVLINK: + FOREACH_DEVICE_DEVLINK(dev, val) + if (token_match_string(token, strempty(startswith(val, "/dev/"))) == (token->op == OP_MATCH)) + return token->op == OP_MATCH; + return token->op == OP_NOMATCH; + case TK_M_NAME: + return token_match_string(token, event->name); + case TK_M_ENV: { + const char *val = NULL; + + (void) device_get_property_value_with_fallback(dev, token->data, properties_list, &val); + + return token_match_string(token, val); + } + case TK_M_CONST: { + const char *val, *k = token->data; + + if (streq(k, "arch")) + val = architecture_to_string(uname_architecture()); + else if (streq(k, "virt")) + val = virtualization_to_string(detect_virtualization()); + else if (streq(k, "cvm")) + val = confidential_virtualization_to_string(detect_confidential_virtualization()); + else + assert_not_reached(); + return token_match_string(token, val); + } + case TK_M_TAG: + case TK_M_PARENTS_TAG: + FOREACH_DEVICE_CURRENT_TAG(dev, val) + if (token_match_string(token, val) == (token->op == OP_MATCH)) + return token->op == OP_MATCH; + return token->op == OP_NOMATCH; + case TK_M_SUBSYSTEM: + case TK_M_PARENTS_SUBSYSTEM: { + const char *val; + + r = sd_device_get_subsystem(dev, &val); + if (r == -ENOENT) + val = NULL; + else if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to get subsystem: %m"); + + return token_match_string(token, val); + } + case TK_M_DRIVER: + case TK_M_PARENTS_DRIVER: { + const char *val; + + r = sd_device_get_driver(dev, &val); + if (r == -ENOENT) + val = NULL; + else if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to get driver: %m"); + + return token_match_string(token, val); + } + case TK_M_ATTR: + case TK_M_PARENTS_ATTR: + return token_match_attr(token, dev, event); + case TK_M_SYSCTL: { + _cleanup_free_ char *value = NULL; + char buf[UDEV_PATH_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "sysctl entry name", token->data, "SYSCTL", /* is_match = */ true); + return false; + } + + r = sysctl_read(sysctl_normalize(buf), &value); + if (r < 0 && r != -ENOENT) + return log_event_error_errno(dev, token, r, "Failed to read sysctl '%s': %m", buf); + + return token_match_string(token, strstrip(value)); + } + case TK_M_TEST: { + mode_t mode = PTR_TO_MODE(token->data); + char buf[UDEV_PATH_SIZE]; + struct stat statbuf; + bool match, truncated; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "file name", token->value, "TEST", /* is_match = */ true); + return false; + } + + if (!path_is_absolute(buf) && + udev_resolve_subsys_kernel(buf, buf, sizeof(buf), false) < 0) { + char tmp[UDEV_PATH_SIZE]; + const char *val; + + r = sd_device_get_syspath(dev, &val); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to get syspath: %m"); + + strscpy_full(tmp, sizeof(tmp), buf, &truncated); + assert(!truncated); + strscpyl_full(buf, sizeof(buf), &truncated, val, "/", tmp, NULL); + if (truncated) + return false; + } + + r = attr_subst_subdir(buf); + if (r == -ENOENT) + return token->op == OP_NOMATCH; + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to test for the existence of '%s': %m", buf); + + if (stat(buf, &statbuf) < 0) + return token->op == OP_NOMATCH; + + if (mode == MODE_INVALID) + return token->op == OP_MATCH; + + match = (statbuf.st_mode & mode) > 0; + return token->op == (match ? OP_MATCH : OP_NOMATCH); + } + case TK_M_PROGRAM: { + char buf[UDEV_LINE_SIZE], result[UDEV_LINE_SIZE]; + bool truncated; + size_t count; + + event->program_result = mfree(event->program_result); + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "command", token->value, "PROGRAM", /* is_match = */ true); + return false; + } + + log_event_debug(dev, token, "Running PROGRAM '%s'", buf); + + r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof(result), NULL); + if (r != 0) { + if (r < 0) + log_event_warning_errno(dev, token, r, "Failed to execute \"%s\": %m", buf); + else /* returned value is positive when program fails */ + log_event_debug(dev, token, "Command \"%s\" returned %d (error)", buf, r); + return token->op == OP_NOMATCH; + } + + delete_trailing_chars(result, "\n"); + count = udev_replace_chars(result, UDEV_ALLOWED_CHARS_INPUT); + if (count > 0) + log_event_debug(dev, token, + "Replaced %zu character(s) in result of \"%s\"", + count, buf); + + event->program_result = strdup(result); + return token->op == OP_MATCH; + } + case TK_M_IMPORT_FILE: { + _cleanup_fclose_ FILE *f = NULL; + char buf[UDEV_PATH_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "file name to be imported", token->value, "IMPORT", /* is_match = */ true); + return false; + } + + log_event_debug(dev, token, "Importing properties from '%s'", buf); + + f = fopen(buf, "re"); + if (!f) { + if (errno != ENOENT) + return log_event_error_errno(dev, token, errno, "Failed to open '%s': %m", buf); + return token->op == OP_NOMATCH; + } + + for (;;) { + _cleanup_free_ char *line = NULL; + char *key, *value; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) { + log_event_debug_errno(dev, token, r, "Failed to read '%s', ignoring: %m", buf); + return token->op == OP_NOMATCH; + } + if (r == 0) + break; + + r = get_property_from_string(line, &key, &value); + if (r < 0) { + log_event_debug_errno(dev, token, r, + "Failed to parse key and value from '%s', ignoring: %m", + line); + continue; + } + if (r == 0) + continue; + + r = device_add_property(dev, key, value); + if (r < 0) + return log_event_error_errno(dev, token, r, + "Failed to add property %s=%s: %m", + key, value); + } + + return token->op == OP_MATCH; + } + case TK_M_IMPORT_PROGRAM: { + _cleanup_strv_free_ char **lines = NULL; + char buf[UDEV_LINE_SIZE], result[UDEV_LINE_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "command", token->value, "IMPORT", /* is_match = */ true); + return false; + } + + log_event_debug(dev, token, "Importing properties from results of '%s'", buf); + + r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof result, &truncated); + if (r != 0) { + if (r < 0) + log_event_warning_errno(dev, token, r, "Failed to execute '%s', ignoring: %m", buf); + else /* returned value is positive when program fails */ + log_event_debug(dev, token, "Command \"%s\" returned %d (error), ignoring", buf, r); + return token->op == OP_NOMATCH; + } + + if (truncated) { + bool found = false; + + /* Drop the last line. */ + for (char *p = PTR_SUB1(buf + strlen(buf), buf); p; p = PTR_SUB1(p, buf)) + if (strchr(NEWLINE, *p)) { + *p = '\0'; + found = true; + } else if (found) + break; + } + + r = strv_split_newlines_full(&lines, result, EXTRACT_RETAIN_ESCAPE); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_event_warning_errno(dev, token, r, + "Failed to extract lines from result of command \"%s\", ignoring: %m", buf); + return false; + } + + STRV_FOREACH(line, lines) { + char *key, *value; + + r = get_property_from_string(*line, &key, &value); + if (r < 0) { + log_event_debug_errno(dev, token, r, + "Failed to parse key and value from '%s', ignoring: %m", + *line); + continue; + } + if (r == 0) + continue; + + r = device_add_property(dev, key, value); + if (r < 0) + return log_event_error_errno(dev, token, r, + "Failed to add property %s=%s: %m", + key, value); + } + + return token->op == OP_MATCH; + } + case TK_M_IMPORT_BUILTIN: { + UdevBuiltinCommand cmd = PTR_TO_UDEV_BUILTIN_CMD(token->data); + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + unsigned mask = 1U << (int) cmd; + char buf[UDEV_LINE_SIZE]; + bool truncated; + + if (udev_builtin_run_once(cmd)) { + /* check if we ran already */ + if (event->builtin_run & mask) { + log_event_debug(dev, token, "Skipping builtin '%s' in IMPORT key", + udev_builtin_name(cmd)); + /* return the result from earlier run */ + return token->op == (event->builtin_ret & mask ? OP_NOMATCH : OP_MATCH); + } + /* mark as ran */ + event->builtin_run |= mask; + } + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "builtin command", token->value, "IMPORT", /* is_match = */ true); + return false; + } + + log_event_debug(dev, token, "Importing properties from results of builtin command '%s'", buf); + + r = udev_builtin_run(event, cmd, buf, false); + if (r < 0) { + /* remember failure */ + log_event_debug_errno(dev, token, r, "Failed to run builtin '%s': %m", buf); + event->builtin_ret |= mask; + } + return token->op == (r >= 0 ? OP_MATCH : OP_NOMATCH); + } + case TK_M_IMPORT_DB: { + const char *val; + + if (!event->dev_db_clone) + return token->op == OP_NOMATCH; + r = sd_device_get_property_value(event->dev_db_clone, token->value, &val); + if (r == -ENOENT) + return token->op == OP_NOMATCH; + if (r < 0) + return log_event_error_errno(dev, token, r, + "Failed to get property '%s' from database: %m", + token->value); + + r = device_add_property(dev, token->value, val); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to add property '%s=%s': %m", + token->value, val); + return token->op == OP_MATCH; + } + case TK_M_IMPORT_CMDLINE: { + _cleanup_free_ char *value = NULL; + + r = proc_cmdline_get_key(token->value, PROC_CMDLINE_VALUE_OPTIONAL|PROC_CMDLINE_IGNORE_EFI_OPTIONS, &value); + if (r < 0) + return log_event_error_errno(dev, token, r, + "Failed to read '%s' option from /proc/cmdline: %m", + token->value); + if (r == 0) + return token->op == OP_NOMATCH; + + r = device_add_property(dev, token->value, value ?: "1"); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to add property '%s=%s': %m", + token->value, value ?: "1"); + return token->op == OP_MATCH; + } + case TK_M_IMPORT_PARENT: { + char buf[UDEV_PATH_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "property name", token->value, "IMPORT", /* is_match = */ true); + return false; + } + + r = import_parent_into_properties(dev, buf); + if (r < 0) + return log_event_error_errno(dev, token, r, + "Failed to import properties '%s' from parent: %m", + buf); + return token->op == (r > 0 ? OP_MATCH : OP_NOMATCH); + } + case TK_M_RESULT: + return token_match_string(token, event->program_result); + case TK_A_OPTIONS_STRING_ESCAPE_NONE: + event->esc = ESCAPE_NONE; + break; + case TK_A_OPTIONS_STRING_ESCAPE_REPLACE: + event->esc = ESCAPE_REPLACE; + break; + case TK_A_OPTIONS_DB_PERSIST: + device_set_db_persist(dev); + break; + case TK_A_OPTIONS_INOTIFY_WATCH: + if (event->inotify_watch_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->inotify_watch_final = true; + + event->inotify_watch = token->data; + break; + case TK_A_OPTIONS_DEVLINK_PRIORITY: + device_set_devlink_priority(dev, PTR_TO_INT(token->data)); + break; + case TK_A_OPTIONS_LOG_LEVEL: { + int level = PTR_TO_INT(token->data); + + if (level < 0) + level = event->default_log_level; + + log_set_max_level(level); + + if (level == LOG_DEBUG && !event->log_level_was_debug) { + /* The log level becomes LOG_DEBUG at first time. Let's log basic information. */ + log_device_uevent(dev, "The log level is changed to 'debug' while processing device"); + event->log_level_was_debug = true; + } + + break; + } + case TK_A_OWNER: { + char owner[UDEV_NAME_SIZE]; + const char *ow = owner; + bool truncated; + + if (event->owner_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->owner_final = true; + + (void) udev_event_apply_format(event, token->value, owner, sizeof(owner), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "user name", token->value, "OWNER", /* is_match = */ false); + break; + } + + r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) + log_unknown_owner(dev, token->rule_line, r, "user", owner); + else + log_event_debug(dev, token, "OWNER %s(%u)", owner, event->uid); + break; + } + case TK_A_GROUP: { + char group[UDEV_NAME_SIZE]; + const char *gr = group; + bool truncated; + + if (event->group_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->group_final = true; + + (void) udev_event_apply_format(event, token->value, group, sizeof(group), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "group name", token->value, "GROUP", /* is_match = */ false); + break; + } + + r = get_group_creds(&gr, &event->gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) + log_unknown_owner(dev, token->rule_line, r, "group", group); + else + log_event_debug(dev, token, "GROUP %s(%u)", group, event->gid); + break; + } + case TK_A_MODE: { + char mode_str[UDEV_NAME_SIZE]; + bool truncated; + + if (event->mode_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->mode_final = true; + + (void) udev_event_apply_format(event, token->value, mode_str, sizeof(mode_str), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "mode", token->value, "MODE", /* is_match = */ false); + break; + } + + r = parse_mode(mode_str, &event->mode); + if (r < 0) + log_event_error_errno(dev, token, r, "Failed to parse mode '%s', ignoring: %m", mode_str); + else + log_event_debug(dev, token, "MODE %#o", event->mode); + break; + } + case TK_A_OWNER_ID: + if (event->owner_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->owner_final = true; + if (!token->data) + break; + event->uid = PTR_TO_UID(token->data); + log_event_debug(dev, token, "OWNER %u", event->uid); + break; + case TK_A_GROUP_ID: + if (event->group_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->group_final = true; + if (!token->data) + break; + event->gid = PTR_TO_GID(token->data); + log_event_debug(dev, token, "GROUP %u", event->gid); + break; + case TK_A_MODE_ID: + if (event->mode_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->mode_final = true; + if (!token->data) + break; + event->mode = PTR_TO_MODE(token->data); + log_event_debug(dev, token, "MODE %#o", event->mode); + break; + case TK_A_SECLABEL: { + _cleanup_free_ char *name = NULL, *label = NULL; + char label_str[UDEV_LINE_SIZE] = {}; + bool truncated; + + name = strdup(token->data); + if (!name) + return log_oom(); + + (void) udev_event_apply_format(event, token->value, label_str, sizeof(label_str), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "security label", token->value, "SECLABEL", /* is_match = */ false); + break; + } + + if (!isempty(label_str)) + label = strdup(label_str); + else + label = strdup(token->value); + if (!label) + return log_oom(); + + if (token->op == OP_ASSIGN) + ordered_hashmap_clear_free_free(event->seclabel_list); + + r = ordered_hashmap_ensure_put(&event->seclabel_list, NULL, name, label); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to store SECLABEL{%s}='%s': %m", name, label); + + log_event_debug(dev, token, "SECLABEL{%s}='%s'", name, label); + + TAKE_PTR(name); + TAKE_PTR(label); + break; + } + case TK_A_ENV: { + const char *val, *name = token->data; + char value_new[UDEV_NAME_SIZE], *p = value_new; + size_t count, l = sizeof(value_new); + bool truncated; + + if (isempty(token->value)) { + if (token->op == OP_ADD) + break; + r = device_add_property(dev, name, NULL); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to remove property '%s': %m", name); + break; + } + + if (token->op == OP_ADD && + device_get_property_value_with_fallback(dev, name, properties_list, &val) >= 0) { + l = strpcpyl_full(&p, l, &truncated, val, " ", NULL); + if (truncated) { + log_event_warning(dev, token, + "The buffer for the property '%s' is full, " + "refusing to append the new value '%s'.", name, token->value); + break; + } + } + + (void) udev_event_apply_format(event, token->value, p, l, false, properties_list, &truncated); + if (truncated) { + _cleanup_free_ char *key_with_name = strjoin("ENV{", name, "}"); + log_event_truncated(dev, token, "property value", token->value, + key_with_name ?: "ENV", /* is_match = */ false); + break; + } + + if (event->esc == ESCAPE_REPLACE) { + count = udev_replace_chars(p, NULL); + if (count > 0) + log_event_debug(dev, token, + "Replaced %zu slash(es) from result of ENV{%s}%s=\"%s\"", + count, name, token->op == OP_ADD ? "+" : "", token->value); + } + + r = device_add_property(dev, name, value_new); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to add property '%s=%s': %m", name, value_new); + break; + } + case TK_A_TAG: { + char buf[UDEV_PATH_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "tag name", token->value, "TAG", /* is_match = */ false); + break; + } + + if (token->op == OP_ASSIGN) + device_cleanup_tags(dev); + + if (token->op == OP_REMOVE) + device_remove_tag(dev, buf); + else { + r = device_add_tag(dev, buf, true); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_event_warning_errno(dev, token, r, "Failed to add tag '%s', ignoring: %m", buf); + } + break; + } + case TK_A_NAME: { + char buf[UDEV_PATH_SIZE]; + bool truncated; + size_t count; + + if (event->name_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->name_final = true; + + if (sd_device_get_ifindex(dev, NULL) < 0) { + log_event_error(dev, token, + "Only network interfaces can be renamed, ignoring NAME=\"%s\".", + token->value); + break; + } + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "network interface name", token->value, "NAME", /* is_match = */ false); + break; + } + + if (IN_SET(event->esc, ESCAPE_UNSET, ESCAPE_REPLACE)) { + if (naming_scheme_has(NAMING_REPLACE_STRICTLY)) + count = udev_replace_ifname(buf); + else + count = udev_replace_chars(buf, "/"); + if (count > 0) + log_event_debug(dev, token, + "Replaced %zu character(s) from result of NAME=\"%s\"", + count, token->value); + } + r = free_and_strdup_warn(&event->name, buf); + if (r < 0) + return r; + + log_event_debug(dev, token, "NAME '%s'", event->name); + break; + } + case TK_A_DEVLINK: { + char buf[UDEV_PATH_SIZE]; + bool truncated; + size_t count; + + if (event->devlink_final) + break; + if (sd_device_get_devnum(dev, NULL) < 0) + break; + if (token->op == OP_ASSIGN_FINAL) + event->devlink_final = true; + if (IN_SET(token->op, OP_ASSIGN, OP_ASSIGN_FINAL)) + device_cleanup_devlinks(dev); + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), + /* replace_whitespace = */ event->esc != ESCAPE_NONE, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "symbolic link path", token->value, "SYMLINK", /* is_match = */ false); + break; + } + + /* By default or string_escape=none, allow multiple symlinks separated by spaces. */ + if (event->esc == ESCAPE_UNSET) + count = udev_replace_chars(buf, /* allow = */ "/ "); + else if (event->esc == ESCAPE_REPLACE) + count = udev_replace_chars(buf, /* allow = */ "/"); + else + count = 0; + if (count > 0) + log_event_debug(dev, token, + "Replaced %zu character(s) from result of SYMLINK=\"%s\"", + count, token->value); + + for (const char *p = buf;;) { + _cleanup_free_ char *path = NULL; + + r = extract_first_word(&p, &path, NULL, EXTRACT_RETAIN_ESCAPE); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_warning_errno(r, "Failed to extract first path in SYMLINK=, ignoring: %m"); + break; + } + if (r == 0) + break; + + if (token->op == OP_REMOVE) { + r = device_remove_devlink(dev, path); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_event_warning_errno(dev, token, r, "Failed to remove devlink '%s', ignoring: %m", path); + else if (r > 0) + log_event_debug(dev, token, "Dropped SYMLINK '%s'", path); + } else { + r = device_add_devlink(dev, path); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + log_event_warning_errno(dev, token, r, "Failed to add devlink '%s', ignoring: %m", path); + else if (r > 0) + log_event_debug(dev, token, "Added SYMLINK '%s'", path); + } + } + break; + } + case TK_A_ATTR: { + char buf[UDEV_PATH_SIZE], value[UDEV_NAME_SIZE]; + const char *val, *key_name = token->data; + bool truncated; + + if (udev_resolve_subsys_kernel(key_name, buf, sizeof(buf), false) < 0 && + sd_device_get_syspath(dev, &val) >= 0) { + strscpyl_full(buf, sizeof(buf), &truncated, val, "/", key_name, NULL); + if (truncated) { + log_event_warning(dev, token, + "The path to the attribute '%s/%s' is too long, refusing to set the attribute.", + val, key_name); + break; + } + } + + r = attr_subst_subdir(buf); + if (r < 0) { + log_event_error_errno(dev, token, r, "Could not find file matches '%s', ignoring: %m", buf); + break; + } + (void) udev_event_apply_format(event, token->value, value, sizeof(value), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "attribute value", token->value, "ATTR", /* is_match = */ false); + break; + } + + log_event_debug(dev, token, "ATTR '%s' writing '%s'", buf, value); + r = write_string_file(buf, value, + WRITE_STRING_FILE_VERIFY_ON_FAILURE | + WRITE_STRING_FILE_DISABLE_BUFFER | + WRITE_STRING_FILE_AVOID_NEWLINE | + WRITE_STRING_FILE_VERIFY_IGNORE_NEWLINE); + if (r < 0) + log_event_error_errno(dev, token, r, "Failed to write ATTR{%s}, ignoring: %m", buf); + break; + } + case TK_A_SYSCTL: { + char buf[UDEV_PATH_SIZE], value[UDEV_NAME_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "sysctl entry name", token->data, "SYSCTL", /* is_match = */ false); + break; + } + + (void) udev_event_apply_format(event, token->value, value, sizeof(value), false, properties_list, &truncated); + if (truncated) { + _cleanup_free_ char *key_with_name = strjoin("SYSCTL{", buf, "}"); + log_event_truncated(dev, token, "sysctl value", token->value, + key_with_name ?: "SYSCTL", /* is_match = */ false); + break; + } + + sysctl_normalize(buf); + log_event_debug(dev, token, "SYSCTL '%s' writing '%s'", buf, value); + r = sysctl_write(buf, value); + if (r < 0) + log_event_error_errno(dev, token, r, "Failed to write SYSCTL{%s}='%s', ignoring: %m", buf, value); + break; + } + case TK_A_RUN_BUILTIN: + case TK_A_RUN_PROGRAM: { + _cleanup_free_ char *cmd = NULL; + char buf[UDEV_LINE_SIZE]; + bool truncated; + + if (event->run_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->run_final = true; + + if (IN_SET(token->op, OP_ASSIGN, OP_ASSIGN_FINAL)) + ordered_hashmap_clear_free_key(event->run_list); + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated); + if (truncated) { + log_event_truncated(dev, token, "command", token->value, + token->type == TK_A_RUN_BUILTIN ? "RUN{builtin}" : "RUN{program}", + /* is_match = */ false); + break; + } + + cmd = strdup(buf); + if (!cmd) + return log_oom(); + + r = ordered_hashmap_ensure_put(&event->run_list, NULL, cmd, token->data); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_event_error_errno(dev, token, r, "Failed to store command '%s': %m", cmd); + + TAKE_PTR(cmd); + + log_event_debug(dev, token, "RUN '%s'", token->value); + break; + } + case TK_A_OPTIONS_STATIC_NODE: + /* do nothing for events. */ + break; + default: + assert_not_reached(); + } + + return true; +} + +static bool token_is_for_parents(UdevRuleToken *token) { + return token->type >= TK_M_PARENTS_KERNEL && token->type <= TK_M_PARENTS_TAG; +} + +static int udev_rule_apply_parent_token_to_event( + UdevRuleToken *head_token, + UdevEvent *event, + int timeout_signal) { + + int r; + + assert(head_token); + assert(event); + + event->dev_parent = ASSERT_PTR(event->dev); + + for (;;) { + LIST_FOREACH(tokens, token, head_token) { + if (!token_is_for_parents(token)) + return true; /* All parent tokens match. */ + + r = udev_rule_apply_token_to_event(token, event->dev_parent, event, 0, timeout_signal, NULL); + if (r < 0) + return r; + if (r == 0) + break; + } + if (r > 0) + /* All parent tokens match, and no more token (except for GOTO) in the line. */ + return true; + + if (sd_device_get_parent(event->dev_parent, &event->dev_parent) < 0) { + event->dev_parent = NULL; + return false; + } + } +} + +static int udev_rule_apply_line_to_event( + UdevRuleLine *line, + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRuleLine **next_line) { + + UdevRuleLineType mask = LINE_HAS_GOTO | LINE_UPDATE_SOMETHING; + bool parents_done = false; + sd_device_action_t action; + int r; + + assert(line); + assert(event); + assert(next_line); + + r = sd_device_get_action(event->dev, &action); + if (r < 0) + return r; + + if (action != SD_DEVICE_REMOVE) { + if (sd_device_get_devnum(event->dev, NULL) >= 0) + mask |= LINE_HAS_DEVLINK; + + if (sd_device_get_ifindex(event->dev, NULL) >= 0) + mask |= LINE_HAS_NAME; + } + + if ((line->type & mask) == 0) + return 0; + + event->esc = ESCAPE_UNSET; + + DEVICE_TRACE_POINT(rules_apply_line, event->dev, line->rule_file->filename, line->line_number); + + LIST_FOREACH(tokens, token, line->tokens) { + if (token_is_for_parents(token)) { + if (parents_done) + continue; + + r = udev_rule_apply_parent_token_to_event(token, event, timeout_signal); + if (r <= 0) + return r; + + parents_done = true; + continue; + } + + r = udev_rule_apply_token_to_event(token, event->dev, event, timeout_usec, timeout_signal, properties_list); + if (r <= 0) + return r; + } + + if (line->goto_line) + *next_line = line->goto_line; /* update next_line only when the line has GOTO token. */ + + return 0; +} + +int udev_rules_apply_to_event( + UdevRules *rules, + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list) { + + int r; + + assert(rules); + assert(event); + + LIST_FOREACH(rule_files, file, rules->rule_files) + LIST_FOREACH_WITH_NEXT(rule_lines, line, next_line, file->rule_lines) { + r = udev_rule_apply_line_to_event(line, event, timeout_usec, timeout_signal, properties_list, &next_line); + if (r < 0) + return r; + } + + return 0; +} + +static int udev_rule_line_apply_static_dev_perms(UdevRuleLine *rule_line) { + _cleanup_strv_free_ char **tags = NULL; + uid_t uid = UID_INVALID; + gid_t gid = GID_INVALID; + mode_t mode = MODE_INVALID; + int r; + + assert(rule_line); + + if (!FLAGS_SET(rule_line->type, LINE_HAS_STATIC_NODE)) + return 0; + + LIST_FOREACH(tokens, token, rule_line->tokens) + if (token->type == TK_A_OWNER_ID) + uid = PTR_TO_UID(token->data); + else if (token->type == TK_A_GROUP_ID) + gid = PTR_TO_GID(token->data); + else if (token->type == TK_A_MODE_ID) + mode = PTR_TO_MODE(token->data); + else if (token->type == TK_A_TAG) { + r = strv_extend(&tags, token->value); + if (r < 0) + return log_oom(); + } else if (token->type == TK_A_OPTIONS_STATIC_NODE) { + r = static_node_apply_permissions(token->value, mode, uid, gid, tags); + if (r < 0) + return r; + } + + return 0; +} + +int udev_rules_apply_static_dev_perms(UdevRules *rules) { + int r; + + assert(rules); + + LIST_FOREACH(rule_files, file, rules->rule_files) + LIST_FOREACH(rule_lines, line, file->rule_lines) { + r = udev_rule_line_apply_static_dev_perms(line); + if (r < 0) + return r; + } + + return 0; +} + +static const char* const resolve_name_timing_table[_RESOLVE_NAME_TIMING_MAX] = { + [RESOLVE_NAME_NEVER] = "never", + [RESOLVE_NAME_LATE] = "late", + [RESOLVE_NAME_EARLY] = "early", +}; + +DEFINE_STRING_TABLE_LOOKUP(resolve_name_timing, ResolveNameTiming); diff --git a/src/udev/udev-rules.h b/src/udev/udev-rules.h new file mode 100644 index 0000000..4352312 --- /dev/null +++ b/src/udev/udev-rules.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "alloc-util.h" +#include "hashmap.h" +#include "time-util.h" + +#define UDEV_NAME_SIZE 512 +#define UDEV_PATH_SIZE 1024 +#define UDEV_LINE_SIZE 16384 + +typedef struct UdevRuleFile UdevRuleFile; +typedef struct UdevRules UdevRules; +typedef struct UdevEvent UdevEvent; + +typedef enum { + ESCAPE_UNSET, + ESCAPE_NONE, /* OPTIONS="string_escape=none" */ + ESCAPE_REPLACE, /* OPTIONS="string_escape=replace" */ + _ESCAPE_TYPE_MAX, + _ESCAPE_TYPE_INVALID = -EINVAL, +} UdevRuleEscapeType; + +typedef enum ResolveNameTiming { + RESOLVE_NAME_NEVER, + RESOLVE_NAME_LATE, + RESOLVE_NAME_EARLY, + _RESOLVE_NAME_TIMING_MAX, + _RESOLVE_NAME_TIMING_INVALID = -EINVAL, +} ResolveNameTiming; + +int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos); +int udev_rules_parse_file(UdevRules *rules, const char *filename, bool extra_checks, UdevRuleFile **ret); +unsigned udev_rule_file_get_issues(UdevRuleFile *rule_file); +UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing); +int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing); +UdevRules *udev_rules_free(UdevRules *rules); +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRules*, udev_rules_free); +#define udev_rules_free_and_replace(a, b) free_and_replace_full(a, b, udev_rules_free) + +bool udev_rules_should_reload(UdevRules *rules); +int udev_rules_apply_to_event(UdevRules *rules, UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list); +int udev_rules_apply_static_dev_perms(UdevRules *rules); + +ResolveNameTiming resolve_name_timing_from_string(const char *s) _pure_; +const char *resolve_name_timing_to_string(ResolveNameTiming i) _const_; diff --git a/src/udev/udev-spawn.c b/src/udev/udev-spawn.c new file mode 100644 index 0000000..67a3005 --- /dev/null +++ b/src/udev/udev-spawn.c @@ -0,0 +1,355 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "sd-event.h" + +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "path-util.h" +#include "process-util.h" +#include "signal-util.h" +#include "string-util.h" +#include "strv.h" +#include "udev-builtin.h" +#include "udev-event.h" +#include "udev-spawn.h" +#include "udev-trace.h" + +typedef struct Spawn { + sd_device *device; + const char *cmd; + pid_t pid; + usec_t timeout_warn_usec; + usec_t timeout_usec; + int timeout_signal; + usec_t event_birth_usec; + bool accept_failure; + int fd_stdout; + int fd_stderr; + char *result; + size_t result_size; + size_t result_len; + bool truncated; +} Spawn; + +static int on_spawn_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Spawn *spawn = ASSERT_PTR(userdata); + char buf[4096], *p; + size_t size; + ssize_t l; + int r; + + assert(fd == spawn->fd_stdout || fd == spawn->fd_stderr); + assert(!spawn->result || spawn->result_len < spawn->result_size); + + if (fd == spawn->fd_stdout && spawn->result) { + p = spawn->result + spawn->result_len; + size = spawn->result_size - spawn->result_len; + } else { + p = buf; + size = sizeof(buf); + } + + l = read(fd, p, size - (p == buf)); + if (l < 0) { + if (errno == EAGAIN) + goto reenable; + + log_device_error_errno(spawn->device, errno, + "Failed to read stdout of '%s': %m", spawn->cmd); + + return 0; + } + + if ((size_t) l == size) { + log_device_warning(spawn->device, "Truncating stdout of '%s' up to %zu byte.", + spawn->cmd, spawn->result_size); + l--; + spawn->truncated = true; + } + + p[l] = '\0'; + if (fd == spawn->fd_stdout && spawn->result) + spawn->result_len += l; + + /* Log output only if we watch stderr. */ + if (l > 0 && spawn->fd_stderr >= 0) { + _cleanup_strv_free_ char **v = NULL; + + r = strv_split_newlines_full(&v, p, EXTRACT_RETAIN_ESCAPE); + if (r < 0) + log_device_debug(spawn->device, + "Failed to split output from '%s'(%s), ignoring: %m", + spawn->cmd, fd == spawn->fd_stdout ? "out" : "err"); + + STRV_FOREACH(q, v) + log_device_debug(spawn->device, "'%s'(%s) '%s'", spawn->cmd, + fd == spawn->fd_stdout ? "out" : "err", *q); + } + + if (l == 0 || spawn->truncated) + return 0; + +reenable: + /* Re-enable the event source if we did not encounter EOF */ + + r = sd_event_source_set_enabled(s, SD_EVENT_ONESHOT); + if (r < 0) + log_device_error_errno(spawn->device, r, + "Failed to reactivate IO source of '%s'", spawn->cmd); + return 0; +} + +static int on_spawn_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + Spawn *spawn = ASSERT_PTR(userdata); + + DEVICE_TRACE_POINT(spawn_timeout, spawn->device, spawn->cmd); + + kill_and_sigcont(spawn->pid, spawn->timeout_signal); + + log_device_error(spawn->device, "Spawned process '%s' ["PID_FMT"] timed out after %s, killing", + spawn->cmd, spawn->pid, + FORMAT_TIMESPAN(spawn->timeout_usec, USEC_PER_SEC)); + + return 1; +} + +static int on_spawn_timeout_warning(sd_event_source *s, uint64_t usec, void *userdata) { + Spawn *spawn = ASSERT_PTR(userdata); + + log_device_warning(spawn->device, "Spawned process '%s' ["PID_FMT"] is taking longer than %s to complete", + spawn->cmd, spawn->pid, + FORMAT_TIMESPAN(spawn->timeout_warn_usec, USEC_PER_SEC)); + + return 1; +} + +static int on_spawn_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata) { + Spawn *spawn = ASSERT_PTR(userdata); + int ret = -EIO; + + switch (si->si_code) { + case CLD_EXITED: + if (si->si_status == 0) + log_device_debug(spawn->device, "Process '%s' succeeded.", spawn->cmd); + else + log_device_full(spawn->device, spawn->accept_failure ? LOG_DEBUG : LOG_WARNING, + "Process '%s' failed with exit code %i.", spawn->cmd, si->si_status); + ret = si->si_status; + break; + case CLD_KILLED: + case CLD_DUMPED: + log_device_error(spawn->device, "Process '%s' terminated by signal %s.", spawn->cmd, signal_to_string(si->si_status)); + break; + default: + log_device_error(spawn->device, "Process '%s' failed due to unknown reason.", spawn->cmd); + } + + DEVICE_TRACE_POINT(spawn_exit, spawn->device, spawn->cmd); + + sd_event_exit(sd_event_source_get_event(s), ret); + return 1; +} + +static int spawn_wait(Spawn *spawn) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *sigchld_source = NULL; + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *stdout_source = NULL; + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *stderr_source = NULL; + int r; + + assert(spawn); + + r = sd_event_new(&e); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to allocate sd-event object: %m"); + + if (spawn->timeout_usec > 0) { + usec_t usec, age_usec; + + usec = now(CLOCK_MONOTONIC); + age_usec = usec - spawn->event_birth_usec; + if (age_usec < spawn->timeout_usec) { + if (spawn->timeout_warn_usec > 0 && + spawn->timeout_warn_usec < spawn->timeout_usec && + spawn->timeout_warn_usec > age_usec) { + spawn->timeout_warn_usec -= age_usec; + + r = sd_event_add_time(e, NULL, CLOCK_MONOTONIC, + usec + spawn->timeout_warn_usec, USEC_PER_SEC, + on_spawn_timeout_warning, spawn); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to create timeout warning event source: %m"); + } + + spawn->timeout_usec -= age_usec; + + r = sd_event_add_time(e, NULL, CLOCK_MONOTONIC, + usec + spawn->timeout_usec, USEC_PER_SEC, on_spawn_timeout, spawn); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to create timeout event source: %m"); + } + } + + if (spawn->fd_stdout >= 0) { + r = sd_event_add_io(e, &stdout_source, spawn->fd_stdout, EPOLLIN, on_spawn_io, spawn); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to create stdio event source: %m"); + r = sd_event_source_set_enabled(stdout_source, SD_EVENT_ONESHOT); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to enable stdio event source: %m"); + } + + if (spawn->fd_stderr >= 0) { + r = sd_event_add_io(e, &stderr_source, spawn->fd_stderr, EPOLLIN, on_spawn_io, spawn); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to create stderr event source: %m"); + r = sd_event_source_set_enabled(stderr_source, SD_EVENT_ONESHOT); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to enable stderr event source: %m"); + } + + r = sd_event_add_child(e, &sigchld_source, spawn->pid, WEXITED, on_spawn_sigchld, spawn); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to create sigchild event source: %m"); + /* SIGCHLD should be processed after IO is complete */ + r = sd_event_source_set_priority(sigchld_source, SD_EVENT_PRIORITY_NORMAL + 1); + if (r < 0) + return log_device_debug_errno(spawn->device, r, "Failed to set priority to sigchild event source: %m"); + + return sd_event_loop(e); +} + +int udev_event_spawn( + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + bool accept_failure, + const char *cmd, + char *result, + size_t result_size, + bool *ret_truncated) { + + _cleanup_close_pair_ int outpipe[2] = EBADF_PAIR, errpipe[2] = EBADF_PAIR; + _cleanup_strv_free_ char **argv = NULL; + char **envp = NULL; + Spawn spawn; + pid_t pid; + int r; + + assert(event); + assert(event->dev); + assert(result || result_size == 0); + + /* pipes from child to parent */ + if (result || log_get_max_level() >= LOG_INFO) + if (pipe2(outpipe, O_NONBLOCK|O_CLOEXEC) != 0) + return log_device_error_errno(event->dev, errno, + "Failed to create pipe for command '%s': %m", cmd); + + if (log_get_max_level() >= LOG_INFO) + if (pipe2(errpipe, O_NONBLOCK|O_CLOEXEC) != 0) + return log_device_error_errno(event->dev, errno, + "Failed to create pipe for command '%s': %m", cmd); + + r = strv_split_full(&argv, cmd, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return log_device_error_errno(event->dev, r, "Failed to split command: %m"); + + if (isempty(argv[0])) + return log_device_error_errno(event->dev, SYNTHETIC_ERRNO(EINVAL), + "Invalid command '%s'", cmd); + + /* allow programs in /usr/lib/udev/ to be called without the path */ + if (!path_is_absolute(argv[0])) { + char *program; + + program = path_join(UDEVLIBEXECDIR, argv[0]); + if (!program) + return log_oom(); + + free_and_replace(argv[0], program); + } + + r = device_get_properties_strv(event->dev, &envp); + if (r < 0) + return log_device_error_errno(event->dev, r, "Failed to get device properties"); + + log_device_debug(event->dev, "Starting '%s'", cmd); + + r = safe_fork_full("(spawn)", + (int[]) { -EBADF, outpipe[WRITE_END], errpipe[WRITE_END] }, + NULL, 0, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE, + &pid); + if (r < 0) + return log_device_error_errno(event->dev, r, + "Failed to fork() to execute command '%s': %m", cmd); + if (r == 0) { + DEVICE_TRACE_POINT(spawn_exec, event->dev, cmd); + execve(argv[0], argv, envp); + _exit(EXIT_FAILURE); + } + + /* parent closed child's ends of pipes */ + outpipe[WRITE_END] = safe_close(outpipe[WRITE_END]); + errpipe[WRITE_END] = safe_close(errpipe[WRITE_END]); + + spawn = (Spawn) { + .device = event->dev, + .cmd = cmd, + .pid = pid, + .accept_failure = accept_failure, + .timeout_warn_usec = udev_warn_timeout(timeout_usec), + .timeout_usec = timeout_usec, + .timeout_signal = timeout_signal, + .event_birth_usec = event->birth_usec, + .fd_stdout = outpipe[READ_END], + .fd_stderr = errpipe[READ_END], + .result = result, + .result_size = result_size, + }; + r = spawn_wait(&spawn); + if (r < 0) + return log_device_error_errno(event->dev, r, + "Failed to wait for spawned command '%s': %m", cmd); + + if (result) + result[spawn.result_len] = '\0'; + + if (ret_truncated) + *ret_truncated = spawn.truncated; + + return r; /* 0 for success, and positive if the program failed */ +} + +void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec, int timeout_signal) { + const char *command; + void *val; + int r; + + ORDERED_HASHMAP_FOREACH_KEY(val, command, event->run_list) { + UdevBuiltinCommand builtin_cmd = PTR_TO_UDEV_BUILTIN_CMD(val); + + if (builtin_cmd != _UDEV_BUILTIN_INVALID) { + log_device_debug(event->dev, "Running built-in command \"%s\"", command); + r = udev_builtin_run(event, builtin_cmd, command, false); + if (r < 0) + log_device_debug_errno(event->dev, r, "Failed to run built-in command \"%s\", ignoring: %m", command); + } else { + if (event->exec_delay_usec > 0) { + log_device_debug(event->dev, "Delaying execution of \"%s\" for %s.", + command, FORMAT_TIMESPAN(event->exec_delay_usec, USEC_PER_SEC)); + (void) usleep_safe(event->exec_delay_usec); + } + + log_device_debug(event->dev, "Running command \"%s\"", command); + + r = udev_event_spawn(event, timeout_usec, timeout_signal, false, command, NULL, 0, NULL); + if (r < 0) + log_device_warning_errno(event->dev, r, "Failed to execute '%s', ignoring: %m", command); + else if (r > 0) /* returned value is positive when program fails */ + log_device_debug(event->dev, "Command \"%s\" returned %d (error), ignoring.", command, r); + } + } +} diff --git a/src/udev/udev-spawn.h b/src/udev/udev-spawn.h new file mode 100644 index 0000000..5efea2e --- /dev/null +++ b/src/udev/udev-spawn.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> +#include <stddef.h> + +#include "macro.h" +#include "time-util.h" + +#define READ_END 0 +#define WRITE_END 1 + +typedef struct UdevEvent UdevEvent; + +int udev_event_spawn( + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + bool accept_failure, + const char *cmd, + char *result, + size_t ressize, + bool *ret_truncated); +void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec, int timeout_signal); + +static inline usec_t udev_warn_timeout(usec_t timeout_usec) { + return DIV_ROUND_UP(timeout_usec, 3); +} diff --git a/src/udev/udev-trace.h b/src/udev/udev-trace.h new file mode 100644 index 0000000..5e94390 --- /dev/null +++ b/src/udev/udev-trace.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#if HAVE_SYS_SDT_H +#define SDT_USE_VARIADIC +#include <sys/sdt.h> + +#include "device-private.h" +#include "device-util.h" +#include "errno-util.h" + +/* Each trace point can have different number of additional arguments. Note that when the macro is used only + * additional arguments are listed in the macro invocation! + * + * Default arguments for each trace point are as follows: + * - arg0 - action + * - arg1 - sysname + * - arg2 - syspath + * - arg3 - subsystem + */ +#define DEVICE_TRACE_POINT(name, dev, ...) \ + do { \ + PROTECT_ERRNO; \ + const char *_n = NULL, *_p = NULL, *_s = NULL; \ + sd_device *_d = (dev); \ + sd_device_action_t _a = _SD_DEVICE_ACTION_INVALID; \ + (void) sd_device_get_action(_d, &_a); \ + (void) sd_device_get_sysname(_d, &_n); \ + (void) sd_device_get_syspath(_d, &_p); \ + (void) sd_device_get_subsystem(_d, &_s); \ + STAP_PROBEV(udev, name, device_action_to_string(_a), _n, _p, _s __VA_OPT__(,) __VA_ARGS__);\ + } while (false); +#else +#define DEVICE_TRACE_POINT(name, dev, ...) ((void) 0) +#endif diff --git a/src/udev/udev-watch.c b/src/udev/udev-watch.c new file mode 100644 index 0000000..58c8279 --- /dev/null +++ b/src/udev/udev-watch.c @@ -0,0 +1,260 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2009 Canonical Ltd. + * Copyright © 2009 Scott James Remnant <scott@netsplit.com> + */ + +#include <sys/inotify.h> + +#include "alloc-util.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "parse-util.h" +#include "rm-rf.h" +#include "stdio-util.h" +#include "string-util.h" +#include "udev-util.h" +#include "udev-watch.h" + +int device_new_from_watch_handle_at(sd_device **ret, int dirfd, int wd) { + char path_wd[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)]; + _cleanup_free_ char *id = NULL; + int r; + + assert(ret); + + if (wd < 0) + return -EBADF; + + if (dirfd >= 0) { + xsprintf(path_wd, "%d", wd); + r = readlinkat_malloc(dirfd, path_wd, &id); + } else { + xsprintf(path_wd, "/run/udev/watch/%d", wd); + r = readlink_malloc(path_wd, &id); + } + if (r < 0) + return r; + + return sd_device_new_from_device_id(ret, id); +} + +int udev_watch_restore(int inotify_fd) { + _cleanup_closedir_ DIR *dir = NULL; + int r; + + /* Move any old watches directory out of the way, and then restore the watches. */ + + assert(inotify_fd >= 0); + + (void) rm_rf("/run/udev/watch.old", REMOVE_ROOT); + + if (rename("/run/udev/watch", "/run/udev/watch.old") < 0) { + if (errno == ENOENT) + return 0; + + r = log_warning_errno(errno, + "Failed to move watches directory '/run/udev/watch/'. " + "Old watches will not be restored: %m"); + goto finalize; + } + + dir = opendir("/run/udev/watch.old"); + if (!dir) { + r = log_warning_errno(errno, + "Failed to open old watches directory '/run/udev/watch.old/'. " + "Old watches will not be restored: %m"); + goto finalize; + } + + FOREACH_DIRENT_ALL(de, dir, break) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + int wd; + + /* For backward compatibility, read symlink from watch handle to device ID. This is necessary + * when udevd is restarted after upgrading from v248 or older. The new format (ID -> wd) was + * introduced by e7f781e473f5119bf9246208a6de9f6b76a39c5d (v249). */ + + if (dot_or_dot_dot(de->d_name)) + continue; + + if (safe_atoi(de->d_name, &wd) < 0) + continue; + + r = device_new_from_watch_handle_at(&dev, dirfd(dir), wd); + if (r < 0) { + log_full_errno(r == -ENODEV ? LOG_DEBUG : LOG_WARNING, r, + "Failed to create sd_device object from saved watch handle '%i', ignoring: %m", + wd); + continue; + } + + (void) udev_watch_begin(inotify_fd, dev); + } + + r = 0; + +finalize: + (void) rm_rf("/run/udev/watch.old", REMOVE_ROOT); + return r; +} + +static int udev_watch_clear(sd_device *dev, int dirfd, int *ret_wd) { + _cleanup_free_ char *wd_str = NULL, *buf = NULL; + const char *id; + int wd = -1, r; + + assert(dev); + assert(dirfd >= 0); + + r = device_get_device_id(dev, &id); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device ID: %m"); + + /* 1. read symlink ID -> wd */ + r = readlinkat_malloc(dirfd, id, &wd_str); + if (r == -ENOENT) { + if (ret_wd) + *ret_wd = -1; + return 0; + } + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to read symlink '/run/udev/watch/%s': %m", id); + goto finalize; + } + + r = safe_atoi(wd_str, &wd); + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to parse watch handle from symlink '/run/udev/watch/%s': %m", id); + goto finalize; + } + + if (wd < 0) { + r = log_device_debug_errno(dev, SYNTHETIC_ERRNO(EBADF), "Invalid watch handle %i.", wd); + goto finalize; + } + + /* 2. read symlink wd -> ID */ + r = readlinkat_malloc(dirfd, wd_str, &buf); + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to read symlink '/run/udev/watch/%s': %m", wd_str); + goto finalize; + } + + /* 3. check if the symlink wd -> ID is owned by the device. */ + if (!streq(buf, id)) { + r = log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENOENT), + "Symlink '/run/udev/watch/%s' is owned by another device '%s'.", wd_str, buf); + goto finalize; + } + + /* 4. remove symlink wd -> ID. + * In the above, we already confirmed that the symlink is owned by us. Hence, no other workers remove + * the symlink and cannot create a new symlink with the same filename but to a different ID. Hence, + * the removal below is safe even the steps in this function are not atomic. */ + if (unlinkat(dirfd, wd_str, 0) < 0 && errno != ENOENT) + log_device_debug_errno(dev, errno, "Failed to remove '/run/udev/watch/%s', ignoring: %m", wd_str); + + if (ret_wd) + *ret_wd = wd; + r = 0; + +finalize: + /* 5. remove symlink ID -> wd. + * The file is always owned by the device. Hence, it is safe to remove it unconditionally. */ + if (unlinkat(dirfd, id, 0) < 0 && errno != ENOENT) + log_device_debug_errno(dev, errno, "Failed to remove '/run/udev/watch/%s': %m", id); + + return r; +} + +int udev_watch_begin(int inotify_fd, sd_device *dev) { + char wd_str[DECIMAL_STR_MAX(int)]; + _cleanup_close_ int dirfd = -EBADF; + const char *devnode, *id; + int wd, r; + + assert(inotify_fd >= 0); + assert(dev); + + if (device_for_action(dev, SD_DEVICE_REMOVE)) + return 0; + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device node: %m"); + + r = device_get_device_id(dev, &id); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device ID: %m"); + + r = dirfd = open_mkdir_at(AT_FDCWD, "/run/udev/watch", O_CLOEXEC | O_RDONLY, 0755); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to create and open '/run/udev/watch/': %m"); + + /* 1. Clear old symlinks */ + (void) udev_watch_clear(dev, dirfd, NULL); + + /* 2. Add inotify watch */ + log_device_debug(dev, "Adding watch on '%s'", devnode); + wd = inotify_add_watch(inotify_fd, devnode, IN_CLOSE_WRITE); + if (wd < 0) + return log_device_debug_errno(dev, errno, "Failed to watch device node '%s': %m", devnode); + + xsprintf(wd_str, "%d", wd); + + /* 3. Create new symlinks */ + if (symlinkat(wd_str, dirfd, id) < 0) { + r = log_device_debug_errno(dev, errno, "Failed to create symlink '/run/udev/watch/%s' to '%s': %m", id, wd_str); + goto on_failure; + } + + if (symlinkat(id, dirfd, wd_str) < 0) { + /* Possibly, the watch handle is previously assigned to another device, and udev_watch_end() + * is not called for the device yet. */ + r = log_device_debug_errno(dev, errno, "Failed to create symlink '/run/udev/watch/%s' to '%s': %m", wd_str, id); + goto on_failure; + } + + return 0; + +on_failure: + (void) unlinkat(dirfd, id, 0); + (void) inotify_rm_watch(inotify_fd, wd); + return r; +} + +int udev_watch_end(int inotify_fd, sd_device *dev) { + _cleanup_close_ int dirfd = -EBADF; + int wd, r; + + assert(dev); + + /* This may be called by 'udevadm test'. In that case, inotify_fd is not initialized. */ + if (inotify_fd < 0) + return 0; + + if (sd_device_get_devname(dev, NULL) < 0) + return 0; + + dirfd = RET_NERRNO(open("/run/udev/watch", O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_RDONLY)); + if (dirfd == -ENOENT) + return 0; + if (dirfd < 0) + return log_device_debug_errno(dev, dirfd, "Failed to open '/run/udev/watch/': %m"); + + /* First, clear symlinks. */ + r = udev_watch_clear(dev, dirfd, &wd); + if (r < 0) + return r; + + /* Then, remove inotify watch. */ + log_device_debug(dev, "Removing watch handle %i.", wd); + (void) inotify_rm_watch(inotify_fd, wd); + + return 0; +} diff --git a/src/udev/udev-watch.h b/src/udev/udev-watch.h new file mode 100644 index 0000000..c454dee --- /dev/null +++ b/src/udev/udev-watch.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "sd-device.h" + +int device_new_from_watch_handle_at(sd_device **ret, int dirfd, int wd); +static inline int device_new_from_watch_handle(sd_device **ret, int wd) { + return device_new_from_watch_handle_at(ret, -1, wd); +} + +int udev_watch_restore(int inotify_fd); +int udev_watch_begin(int inotify_fd, sd_device *dev); +int udev_watch_end(int inotify_fd, sd_device *dev); diff --git a/src/udev/udev-worker.c b/src/udev/udev-worker.c new file mode 100644 index 0000000..53722b2 --- /dev/null +++ b/src/udev/udev-worker.c @@ -0,0 +1,352 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <sys/file.h> +#include <sys/ioctl.h> +#include <sys/mount.h> + +#include "alloc-util.h" +#include "blockdev-util.h" +#include "common-signal.h" +#include "device-monitor-private.h" +#include "device-private.h" +#include "device-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "io-util.h" +#include "path-util.h" +#include "process-util.h" +#include "signal-util.h" +#include "string-util.h" +#include "udev-event.h" +#include "udev-spawn.h" +#include "udev-trace.h" +#include "udev-util.h" +#include "udev-watch.h" +#include "udev-worker.h" + +void udev_worker_done(UdevWorker *worker) { + assert(worker); + + sd_event_unref(worker->event); + sd_netlink_unref(worker->rtnl); + sd_device_monitor_unref(worker->monitor); + hashmap_free(worker->properties); + udev_rules_free(worker->rules); + safe_close(worker->pipe_fd); +} + +int udev_get_whole_disk(sd_device *dev, sd_device **ret_device, const char **ret_devname) { + const char *val; + int r; + + assert(dev); + + if (device_for_action(dev, SD_DEVICE_REMOVE)) + goto irrelevant; + + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + /* Exclude the following devices: + * For "dm-", see the comment added by e918a1b5a94f270186dca59156354acd2a596494. + * For "md", see the commit message of 2e5b17d01347d3c3118be2b8ad63d20415dbb1f0, + * but not sure the assumption is still valid even when partitions are created on the md + * devices, surprisingly which seems to be possible, see PR #22973. + * For "drbd", see the commit message of fee854ee8ccde0cd28e0f925dea18cce35f3993d. */ + if (STARTSWITH_SET(val, "dm-", "md", "drbd")) + goto irrelevant; + + r = block_device_get_whole_disk(dev, &dev); + if (IN_SET(r, + -ENOTBLK, /* The device is not a block device. */ + -ENODEV /* The whole disk device was not found, it may already be removed. */)) + goto irrelevant; + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get whole disk device: %m"); + + r = sd_device_get_devname(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devname: %m"); + + if (ret_device) + *ret_device = dev; + if (ret_devname) + *ret_devname = val; + return 1; + +irrelevant: + if (ret_device) + *ret_device = NULL; + if (ret_devname) + *ret_devname = NULL; + return 0; +} + +static int worker_lock_whole_disk(sd_device *dev, int *ret_fd) { + _cleanup_close_ int fd = -EBADF; + sd_device *dev_whole_disk; + const char *val; + int r; + + assert(dev); + assert(ret_fd); + + /* Take a shared lock on the device node; this establishes a concept of device "ownership" to + * serialize device access. External processes holding an exclusive lock will cause udev to skip the + * event handling; in the case udev acquired the lock, the external process can block until udev has + * finished its event handling. */ + + r = udev_get_whole_disk(dev, &dev_whole_disk, &val); + if (r < 0) + return r; + if (r == 0) + goto nolock; + + fd = sd_device_open(dev_whole_disk, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) { + bool ignore = ERRNO_IS_DEVICE_ABSENT(fd); + + log_device_debug_errno(dev, fd, "Failed to open '%s'%s: %m", val, ignore ? ", ignoring" : ""); + if (!ignore) + return fd; + + goto nolock; + } + + if (flock(fd, LOCK_SH|LOCK_NB) < 0) + return log_device_debug_errno(dev, errno, "Failed to flock(%s): %m", val); + + *ret_fd = TAKE_FD(fd); + return 1; + +nolock: + *ret_fd = -EBADF; + return 0; +} + +static int worker_mark_block_device_read_only(sd_device *dev) { + _cleanup_close_ int fd = -EBADF; + const char *val; + int state = 1, r; + + assert(dev); + + /* Do this only once, when the block device is new. If the device is later retriggered let's not + * toggle the bit again, so that people can boot up with full read-only mode and then unset the bit + * for specific devices only. */ + if (!device_for_action(dev, SD_DEVICE_ADD)) + return 0; + + r = sd_device_get_subsystem(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get subsystem: %m"); + + if (!streq(val, "block")) + return 0; + + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + /* Exclude synthetic devices for now, this is supposed to be a safety feature to avoid modification + * of physical devices, and what sits on top of those doesn't really matter if we don't allow the + * underlying block devices to receive changes. */ + if (STARTSWITH_SET(val, "dm-", "md", "drbd", "loop", "nbd", "zram")) + return 0; + + fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return log_device_debug_errno(dev, fd, "Failed to open '%s', ignoring: %m", val); + + if (ioctl(fd, BLKROSET, &state) < 0) + return log_device_warning_errno(dev, errno, "Failed to mark block device '%s' read-only: %m", val); + + log_device_info(dev, "Successfully marked block device '%s' read-only.", val); + return 0; +} + +static int worker_process_device(UdevWorker *worker, sd_device *dev) { + _cleanup_(udev_event_freep) UdevEvent *udev_event = NULL; + _cleanup_close_ int fd_lock = -EBADF; + int r; + + assert(worker); + assert(dev); + + log_device_uevent(dev, "Processing device"); + + udev_event = udev_event_new(dev, worker->exec_delay_usec, worker->rtnl, worker->log_level); + if (!udev_event) + return -ENOMEM; + + /* If this is a block device and the device is locked currently via the BSD advisory locks, + * someone else is using it exclusively. We don't run our udev rules now to not interfere. + * Instead of processing the event, we requeue the event and will try again after a delay. + * + * The user-facing side of this: https://systemd.io/BLOCK_DEVICE_LOCKING */ + r = worker_lock_whole_disk(dev, &fd_lock); + if (r == -EAGAIN) + return EVENT_RESULT_TRY_AGAIN; + if (r < 0) + return r; + + if (worker->blockdev_read_only) + (void) worker_mark_block_device_read_only(dev); + + /* apply rules, create node, symlinks */ + r = udev_event_execute_rules( + udev_event, + worker->inotify_fd, + worker->timeout_usec, + worker->timeout_signal, + worker->properties, + worker->rules); + if (r < 0) + return r; + + udev_event_execute_run(udev_event, worker->timeout_usec, worker->timeout_signal); + + if (!worker->rtnl) + /* in case rtnl was initialized */ + worker->rtnl = sd_netlink_ref(udev_event->rtnl); + + if (udev_event->inotify_watch) { + r = udev_watch_begin(worker->inotify_fd, dev); + if (r < 0 && r != -ENOENT) /* The device may be already removed, ignore -ENOENT. */ + log_device_warning_errno(dev, r, "Failed to add inotify watch, ignoring: %m"); + } + + log_device_uevent(dev, "Device processed"); + return 0; +} + +void udev_broadcast_result(sd_device_monitor *monitor, sd_device *dev, EventResult result) { + int r; + + assert(dev); + + /* On exit, manager->monitor is already NULL. */ + if (!monitor) + return; + + if (result != EVENT_RESULT_SUCCESS) { + (void) device_add_property(dev, "UDEV_WORKER_FAILED", "1"); + + switch (result) { + case EVENT_RESULT_NERRNO_MIN ... EVENT_RESULT_NERRNO_MAX: { + const char *str; + + (void) device_add_propertyf(dev, "UDEV_WORKER_ERRNO", "%i", -result); + + str = errno_to_name(result); + if (str) + (void) device_add_property(dev, "UDEV_WORKER_ERRNO_NAME", str); + break; + } + case EVENT_RESULT_EXIT_STATUS_BASE ... EVENT_RESULT_EXIT_STATUS_MAX: + (void) device_add_propertyf(dev, "UDEV_WORKER_EXIT_STATUS", "%i", result - EVENT_RESULT_EXIT_STATUS_BASE); + break; + + case EVENT_RESULT_TRY_AGAIN: + assert_not_reached(); + break; + + case EVENT_RESULT_SIGNAL_BASE ... EVENT_RESULT_SIGNAL_MAX: { + const char *str; + + (void) device_add_propertyf(dev, "UDEV_WORKER_SIGNAL", "%i", result - EVENT_RESULT_SIGNAL_BASE); + + str = signal_to_string(result - EVENT_RESULT_SIGNAL_BASE); + if (str) + (void) device_add_property(dev, "UDEV_WORKER_SIGNAL_NAME", str); + break; + } + default: + log_device_warning(dev, "Unknown event result \"%i\", ignoring.", result); + } + } + + r = device_monitor_send_device(monitor, NULL, dev); + if (r < 0) + log_device_warning_errno(dev, r, + "Failed to broadcast event to libudev listeners, ignoring: %m"); +} + +static int worker_send_result(UdevWorker *worker, EventResult result) { + assert(worker); + assert(worker->pipe_fd >= 0); + + return loop_write(worker->pipe_fd, &result, sizeof(result)); +} + +static int worker_device_monitor_handler(sd_device_monitor *monitor, sd_device *dev, void *userdata) { + UdevWorker *worker = ASSERT_PTR(userdata); + int r; + + assert(dev); + + r = worker_process_device(worker, dev); + if (r == EVENT_RESULT_TRY_AGAIN) + /* if we couldn't acquire the flock(), then requeue the event */ + log_device_debug(dev, "Block device is currently locked, requeueing the event."); + else { + if (r < 0) + log_device_warning_errno(dev, r, "Failed to process device, ignoring: %m"); + + /* send processed event back to libudev listeners */ + udev_broadcast_result(monitor, dev, r); + } + + /* send udevd the result of the event execution */ + r = worker_send_result(worker, r); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to send signal to main daemon, ignoring: %m"); + + /* Reset the log level, as it might be changed by "OPTIONS=log_level=". */ + log_set_max_level(worker->log_level); + + return 1; +} + +int udev_worker_main(UdevWorker *worker, sd_device *dev) { + int r; + + assert(worker); + assert(worker->monitor); + assert(dev); + + DEVICE_TRACE_POINT(worker_spawned, dev, getpid_cached()); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, -1) >= 0); + + /* Reset OOM score, we only protect the main daemon. */ + r = set_oom_score_adjust(0); + if (r < 0) + log_debug_errno(r, "Failed to reset OOM score, ignoring: %m"); + + r = sd_event_new(&worker->event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = sd_event_add_signal(worker->event, NULL, SIGTERM, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to set SIGTERM event: %m"); + + r = sd_device_monitor_attach_event(worker->monitor, worker->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event loop to device monitor: %m"); + + r = sd_device_monitor_start(worker->monitor, worker_device_monitor_handler, worker); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + /* Process first device */ + (void) worker_device_monitor_handler(worker->monitor, dev, worker); + + r = sd_event_loop(worker->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} diff --git a/src/udev/udev-worker.h b/src/udev/udev-worker.h new file mode 100644 index 0000000..05c319e --- /dev/null +++ b/src/udev/udev-worker.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sd-device.h" +#include "sd-event.h" +#include "sd-netlink.h" + +#include "errno-list.h" +#include "hashmap.h" +#include "time-util.h" + +typedef struct UdevRules UdevRules; + +typedef struct UdevWorker { + sd_event *event; + sd_netlink *rtnl; + sd_device_monitor *monitor; + + Hashmap *properties; + UdevRules *rules; + + int pipe_fd; + int inotify_fd; /* Do not close! */ + + usec_t exec_delay_usec; + usec_t timeout_usec; + int timeout_signal; + int log_level; + bool blockdev_read_only; +} UdevWorker; + +/* passed from worker to main process */ +typedef enum EventResult { + EVENT_RESULT_NERRNO_MIN = -ERRNO_MAX, + EVENT_RESULT_NERRNO_MAX = -1, + EVENT_RESULT_SUCCESS = 0, + EVENT_RESULT_EXIT_STATUS_BASE = 0, + EVENT_RESULT_EXIT_STATUS_MAX = 255, + EVENT_RESULT_TRY_AGAIN = 256, /* when the block device is locked by another process. */ + EVENT_RESULT_SIGNAL_BASE = 257, + EVENT_RESULT_SIGNAL_MAX = EVENT_RESULT_SIGNAL_BASE + _NSIG, + _EVENT_RESULT_MAX, + _EVENT_RESULT_INVALID = -EINVAL, +} EventResult; + +void udev_worker_done(UdevWorker *worker); +int udev_worker_main(UdevWorker *worker, sd_device *dev); + +void udev_broadcast_result(sd_device_monitor *monitor, sd_device *dev, EventResult result); +int udev_get_whole_disk(sd_device *dev, sd_device **ret_device, const char **ret_devname); diff --git a/src/udev/udev.conf b/src/udev/udev.conf new file mode 100644 index 0000000..07d7f0c --- /dev/null +++ b/src/udev/udev.conf @@ -0,0 +1,11 @@ +# see udev.conf(5) for details +# +# udevd is also started in the initrd. When this file is modified you might +# also want to rebuild the initrd, so that it will include the modified configuration. + +#udev_log=info +#children_max= +#exec_delay= +#event_timeout=180 +#timeout_signal=SIGKILL +#resolve_names=early diff --git a/src/udev/udev.pc.in b/src/udev/udev.pc.in new file mode 100644 index 0000000..cbf7693 --- /dev/null +++ b/src/udev/udev.pc.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +Name: udev +Description: udev +Version: {{PROJECT_VERSION}} + +udev_dir={{UDEVLIBEXECDIR}} +udevdir=${udev_dir} diff --git a/src/udev/udevadm-control.c b/src/udev/udevadm-control.c new file mode 100644 index 0000000..64615f5 --- /dev/null +++ b/src/udev/udevadm-control.c @@ -0,0 +1,239 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "parse-util.h" +#include "process-util.h" +#include "static-destruct.h" +#include "strv.h" +#include "syslog-util.h" +#include "time-util.h" +#include "udevadm.h" +#include "udev-ctrl.h" +#include "virt.h" + +static char **arg_env = NULL; +static usec_t arg_timeout = 60 * USEC_PER_SEC; +static bool arg_ping = false; +static bool arg_reload = false; +static bool arg_exit = false; +static int arg_max_children = -1; +static int arg_log_level = -1; +static int arg_start_exec_queue = -1; + +STATIC_DESTRUCTOR_REGISTER(arg_env, strv_freep); + +static int help(void) { + printf("%s control OPTION\n\n" + "Control the udev daemon.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -e --exit Instruct the daemon to cleanup and exit\n" + " -l --log-level=LEVEL Set the udev log level for the daemon\n" + " -s --stop-exec-queue Do not execute events, queue only\n" + " -S --start-exec-queue Execute events, flush queue\n" + " -R --reload Reload rules and databases\n" + " -p --property=KEY=VALUE Set a global property for all events\n" + " -m --children-max=N Maximum number of children\n" + " --ping Wait for udev to respond to a ping message\n" + " -t --timeout=SECONDS Maximum time to block for a reply\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_PING = 0x100, + }; + + static const struct option options[] = { + { "exit", no_argument, NULL, 'e' }, + { "log-level", required_argument, NULL, 'l' }, + { "log-priority", required_argument, NULL, 'l' }, /* for backward compatibility */ + { "stop-exec-queue", no_argument, NULL, 's' }, + { "start-exec-queue", no_argument, NULL, 'S' }, + { "reload", no_argument, NULL, 'R' }, + { "reload-rules", no_argument, NULL, 'R' }, /* alias for -R */ + { "property", required_argument, NULL, 'p' }, + { "env", required_argument, NULL, 'p' }, /* alias for -p */ + { "children-max", required_argument, NULL, 'm' }, + { "ping", no_argument, NULL, ARG_PING }, + { "timeout", required_argument, NULL, 't' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + if (argc <= 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This command expects one or more options."); + + while ((c = getopt_long(argc, argv, "el:sSRp:m:t:Vh", options, NULL)) >= 0) + switch (c) { + + case 'e': + arg_exit = true; + break; + + case 'l': + arg_log_level = log_level_from_string(optarg); + if (arg_log_level < 0) + return log_error_errno(arg_log_level, "Failed to parse log level '%s': %m", optarg); + break; + + case 's': + arg_start_exec_queue = false; + break; + + case 'S': + arg_start_exec_queue = true; + break; + + case 'R': + arg_reload = true; + break; + + case 'p': + if (!strchr(optarg, '=')) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "expect <KEY>=<value> instead of '%s'", optarg); + + r = strv_extend(&arg_env, optarg); + if (r < 0) + return log_error_errno(r, "Failed to extend environment: %m"); + + break; + + case 'm': { + unsigned i; + r = safe_atou(optarg, &i); + if (r < 0) + return log_error_errno(r, "Failed to parse maximum number of children '%s': %m", optarg); + arg_max_children = i; + break; + } + + case ARG_PING: + arg_ping = true; + break; + + case 't': + r = parse_sec(optarg, &arg_timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg); + break; + + case 'V': + return print_version(); + + case 'h': + return help(); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (optind < argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Extraneous argument: %s", argv[optind]); + + return 1; +} + +int control_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_ctrl_unrefp) UdevCtrl *uctrl = NULL; + int r; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = udev_ctrl_new(&uctrl); + if (r < 0) + return log_error_errno(r, "Failed to initialize udev control: %m"); + + if (arg_exit) { + r = udev_ctrl_send_exit(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to send exit request: %m"); + return 0; + } + + if (arg_log_level >= 0) { + r = udev_ctrl_send_set_log_level(uctrl, arg_log_level); + if (r < 0) + return log_error_errno(r, "Failed to send request to set log level: %m"); + } + + if (arg_start_exec_queue == false) { + r = udev_ctrl_send_stop_exec_queue(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to send request to stop exec queue: %m"); + } + + if (arg_start_exec_queue == true) { + r = udev_ctrl_send_start_exec_queue(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to send request to start exec queue: %m"); + } + + if (arg_reload) { + r = udev_ctrl_send_reload(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to send reload request: %m"); + } + + STRV_FOREACH(env, arg_env) { + r = udev_ctrl_send_set_env(uctrl, *env); + if (r < 0) + return log_error_errno(r, "Failed to send request to update environment: %m"); + } + + if (arg_max_children >= 0) { + r = udev_ctrl_send_set_children_max(uctrl, arg_max_children); + if (r < 0) + return log_error_errno(r, "Failed to send request to set number of children: %m"); + } + + if (arg_ping) { + r = udev_ctrl_send_ping(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to send a ping message: %m"); + } + + r = udev_ctrl_wait(uctrl, arg_timeout); + if (r < 0) + return log_error_errno(r, "Failed to wait for daemon to reply: %m"); + + return 0; +} diff --git a/src/udev/udevadm-hwdb.c b/src/udev/udevadm-hwdb.c new file mode 100644 index 0000000..2f5429f --- /dev/null +++ b/src/udev/udevadm-hwdb.c @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> + +#include "hwdb-util.h" +#include "udevadm.h" + +static const char *arg_test = NULL; +static const char *arg_root = NULL; +static const char *arg_hwdb_bin_dir = NULL; +static bool arg_update = false; +static bool arg_strict = false; + +static int help(void) { + printf("%s hwdb [OPTIONS]\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -u --update Update the hardware database\n" + " -s --strict When updating, return non-zero exit value on any parsing error\n" + " --usr Generate in " UDEVLIBEXECDIR " instead of /etc/udev\n" + " -t --test=MODALIAS Query database and print result\n" + " -r --root=PATH Alternative root path in the filesystem\n\n" + "NOTE:\n" + "The sub-command 'hwdb' is deprecated, and is left for backwards compatibility.\n" + "Please use systemd-hwdb instead.\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_USR = 0x100, + }; + + static const struct option options[] = { + { "update", no_argument, NULL, 'u' }, + { "usr", no_argument, NULL, ARG_USR }, + { "strict", no_argument, NULL, 's' }, + { "test", required_argument, NULL, 't' }, + { "root", required_argument, NULL, 'r' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int c; + + while ((c = getopt_long(argc, argv, "ust:r:Vh", options, NULL)) >= 0) + switch (c) { + case 'u': + arg_update = true; + break; + case ARG_USR: + arg_hwdb_bin_dir = UDEVLIBEXECDIR; + break; + case 's': + arg_strict = true; + break; + case 't': + arg_test = optarg; + break; + case 'r': + arg_root = optarg; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + return 1; +} + +int hwdb_main(int argc, char *argv[], void *userdata) { + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (!arg_update && !arg_test) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Either --update or --test must be used."); + + log_notice("udevadm hwdb is deprecated. Use systemd-hwdb instead."); + + if (arg_update) { + r = hwdb_update(arg_root, arg_hwdb_bin_dir, arg_strict, true); + if (r < 0) + return r; + } + + if (arg_test) + return hwdb_query(arg_test, NULL); + + return 0; +} diff --git a/src/udev/udevadm-info.c b/src/udev/udevadm-info.c new file mode 100644 index 0000000..4cd9ad4 --- /dev/null +++ b/src/udev/udevadm-info.c @@ -0,0 +1,1120 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "sd-device.h" + +#include "alloc-util.h" +#include "device-enumerator-private.h" +#include "device-private.h" +#include "device-util.h" +#include "devnum-util.h" +#include "dirent-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "glyph-util.h" +#include "json.h" +#include "pager.h" +#include "parse-argument.h" +#include "sort-util.h" +#include "static-destruct.h" +#include "string-table.h" +#include "string-util.h" +#include "terminal-util.h" +#include "udev-util.h" +#include "udevadm.h" +#include "udevadm-util.h" + +typedef enum ActionType { + ACTION_QUERY, + ACTION_ATTRIBUTE_WALK, + ACTION_DEVICE_ID_FILE, + ACTION_TREE, + ACTION_EXPORT, +} ActionType; + +typedef enum QueryType { + QUERY_NAME, + QUERY_PATH, + QUERY_SYMLINK, + QUERY_PROPERTY, + QUERY_ALL, +} QueryType; + +static char **arg_properties = NULL; +static bool arg_root = false; +static bool arg_export = false; +static bool arg_value = false; +static const char *arg_export_prefix = NULL; +static usec_t arg_wait_for_initialization_timeout = 0; +PagerFlags arg_pager_flags = 0; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; + +/* Put a limit on --tree descent level to not exhaust our stack */ +#define TREE_DEPTH_MAX 64 + +static bool skip_attribute(const char *name) { + assert(name); + + /* Those are either displayed separately or should not be shown at all. */ + return STR_IN_SET(name, + "uevent", + "dev", + "modalias", + "resource", + "driver", + "subsystem", + "module"); +} + +typedef struct SysAttr { + const char *name; + const char *value; +} SysAttr; + +STATIC_DESTRUCTOR_REGISTER(arg_properties, strv_freep); + +static int sysattr_compare(const SysAttr *a, const SysAttr *b) { + assert(a); + assert(b); + + return strcmp(a->name, b->name); +} + +static int print_all_attributes(sd_device *device, bool is_parent) { + _cleanup_free_ SysAttr *sysattrs = NULL; + const char *value; + size_t n_items = 0; + int r; + + assert(device); + + value = NULL; + (void) sd_device_get_devpath(device, &value); + printf(" looking at %sdevice '%s':\n", is_parent ? "parent " : "", strempty(value)); + + value = NULL; + (void) sd_device_get_sysname(device, &value); + printf(" %s==\"%s\"\n", is_parent ? "KERNELS" : "KERNEL", strempty(value)); + + value = NULL; + (void) sd_device_get_subsystem(device, &value); + printf(" %s==\"%s\"\n", is_parent ? "SUBSYSTEMS" : "SUBSYSTEM", strempty(value)); + + value = NULL; + (void) sd_device_get_driver(device, &value); + printf(" %s==\"%s\"\n", is_parent ? "DRIVERS" : "DRIVER", strempty(value)); + + FOREACH_DEVICE_SYSATTR(device, name) { + size_t len; + + if (skip_attribute(name)) + continue; + + r = sd_device_get_sysattr_value(device, name, &value); + if (r >= 0) { + /* skip any values that look like a path */ + if (value[0] == '/') + continue; + + /* skip nonprintable attributes */ + len = strlen(value); + while (len > 0 && isprint((unsigned char) value[len-1])) + len--; + if (len > 0) + continue; + + } else if (ERRNO_IS_PRIVILEGE(r)) + value = "(not readable)"; + else + continue; + + if (!GREEDY_REALLOC(sysattrs, n_items + 1)) + return log_oom(); + + sysattrs[n_items] = (SysAttr) { + .name = name, + .value = value, + }; + n_items++; + } + + typesafe_qsort(sysattrs, n_items, sysattr_compare); + + for (size_t i = 0; i < n_items; i++) + printf(" %s{%s}==\"%s\"\n", is_parent ? "ATTRS" : "ATTR", sysattrs[i].name, sysattrs[i].value); + + puts(""); + + return 0; +} + +static int print_device_chain(sd_device *device) { + sd_device *child, *parent; + int r; + + assert(device); + + printf("\n" + "Udevadm info starts with the device specified by the devpath and then\n" + "walks up the chain of parent devices. It prints for every device\n" + "found, all possible attributes in the udev rules key format.\n" + "A rule to match, can be composed by the attributes of the device\n" + "and the attributes from one single parent device.\n" + "\n"); + + r = print_all_attributes(device, false); + if (r < 0) + return r; + + for (child = device; sd_device_get_parent(child, &parent) >= 0; child = parent) { + r = print_all_attributes(parent, true); + if (r < 0) + return r; + } + + return 0; +} + +static int print_record(sd_device *device, const char *prefix) { + const char *str, *subsys; + dev_t devnum; + uint64_t q; + int i, ifi; + + assert(device); + + prefix = strempty(prefix); + + /* We don't show syspath here, because it's identical to devpath (modulo the "/sys" prefix). + * + * We don't show action/seqnum here because that only makes sense for records synthesized from + * uevents, not for those synthesized from database entries. + * + * We don't show sysattrs here, because they can be expensive and potentially issue expensive driver + * IO. + * + * Coloring: let's be conservative with coloring. Let's use it to group related fields. Right now: + * + * • white for fields that give the device a name + * • green for fields that categorize the device into subsystem/devtype and similar + * • cyan for fields about associated device nodes/symlinks/network interfaces and such + * • magenta for block device diskseq + * • yellow for driver info + * • no color for regular properties */ + + assert_se(sd_device_get_devpath(device, &str) >= 0); + printf("%sP: %s%s%s\n", prefix, ansi_highlight_white(), str, ansi_normal()); + + if (sd_device_get_sysname(device, &str) >= 0) + printf("%sM: %s%s%s\n", prefix, ansi_highlight_white(), str, ansi_normal()); + + if (sd_device_get_sysnum(device, &str) >= 0) + printf("%sR: %s%s%s\n", prefix, ansi_highlight_white(), str, ansi_normal()); + + if (sd_device_get_subsystem(device, &subsys) >= 0) + printf("%sU: %s%s%s\n", prefix, ansi_highlight_green(), subsys, ansi_normal()); + + if (sd_device_get_devtype(device, &str) >= 0) + printf("%sT: %s%s%s\n", prefix, ansi_highlight_green(), str, ansi_normal()); + + if (sd_device_get_devnum(device, &devnum) >= 0) + printf("%sD: %s%c %u:%u%s\n", + prefix, + ansi_highlight_cyan(), + streq_ptr(subsys, "block") ? 'b' : 'c', major(devnum), minor(devnum), + ansi_normal()); + + if (sd_device_get_ifindex(device, &ifi) >= 0) + printf("%sI: %s%i%s\n", prefix, ansi_highlight_cyan(), ifi, ansi_normal()); + + if (sd_device_get_devname(device, &str) >= 0) { + const char *val; + + assert_se(val = path_startswith(str, "/dev/")); + printf("%sN: %s%s%s\n", prefix, ansi_highlight_cyan(), val, ansi_normal()); + + if (device_get_devlink_priority(device, &i) >= 0) + printf("%sL: %s%i%s\n", prefix, ansi_highlight_cyan(), i, ansi_normal()); + + FOREACH_DEVICE_DEVLINK(device, link) { + assert_se(val = path_startswith(link, "/dev/")); + printf("%sS: %s%s%s\n", prefix, ansi_highlight_cyan(), val, ansi_normal()); + } + } + + if (sd_device_get_diskseq(device, &q) >= 0) + printf("%sQ: %s%" PRIu64 "%s\n", prefix, ansi_highlight_magenta(), q, ansi_normal()); + + if (sd_device_get_driver(device, &str) >= 0) + printf("%sV: %s%s%s\n", prefix, ansi_highlight_yellow4(), str, ansi_normal()); + + FOREACH_DEVICE_PROPERTY(device, key, val) + printf("%sE: %s=%s\n", prefix, key, val); + + if (isempty(prefix)) + puts(""); + return 0; +} + +static int record_to_json(sd_device *device, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + const char *str; + int r; + + assert(device); + assert(ret); + + /* We don't show any shorthand fields here as done in print_record() except for SYSNAME and SYSNUM as + * all the other ones have a matching property which will already be included. */ + + if (sd_device_get_sysname(device, &str) >= 0) { + r = json_variant_set_field_string(&v, "SYSNAME", str); + if (r < 0) + return r; + } + + if (sd_device_get_sysnum(device, &str) >= 0) { + r = json_variant_set_field_string(&v, "SYSNUM", str); + if (r < 0) + return r; + } + + FOREACH_DEVICE_PROPERTY(device, key, val) { + r = json_variant_set_field_string(&v, key, val); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(v); + return 0; +} + +static int stat_device(const char *name, bool export, const char *prefix) { + struct stat statbuf; + + assert(name); + + if (stat(name, &statbuf) != 0) + return -errno; + + if (export) { + if (!prefix) + prefix = "INFO_"; + printf("%sMAJOR=%u\n" + "%sMINOR=%u\n", + prefix, major(statbuf.st_dev), + prefix, minor(statbuf.st_dev)); + } else + printf("%u:%u\n", major(statbuf.st_dev), minor(statbuf.st_dev)); + return 0; +} + +static int export_devices(sd_device_enumerator *e) { + sd_device *d; + int r; + + assert(e); + + r = device_enumerator_scan_devices(e); + if (r < 0) + return log_error_errno(r, "Failed to scan devices: %m"); + + pager_open(arg_pager_flags); + + FOREACH_DEVICE_AND_SUBSYSTEM(e, d) + if (arg_json_format_flags & JSON_FORMAT_OFF) + (void) print_record(d, NULL); + else { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + r = record_to_json(d, &v); + if (r < 0) + return r; + + (void) json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } + + return 0; +} + +static void cleanup_dir(DIR *dir, mode_t mask, int depth) { + assert(dir); + + if (depth <= 0) + return; + + FOREACH_DIRENT_ALL(dent, dir, break) { + struct stat stats; + + if (dot_or_dot_dot(dent->d_name)) + continue; + if (fstatat(dirfd(dir), dent->d_name, &stats, AT_SYMLINK_NOFOLLOW) < 0) + continue; + if ((stats.st_mode & mask) != 0) + continue; + if (S_ISDIR(stats.st_mode)) { + _cleanup_closedir_ DIR *subdir = NULL; + + subdir = xopendirat(dirfd(dir), dent->d_name, O_NOFOLLOW); + if (!subdir) + log_debug_errno(errno, "Failed to open subdirectory '%s', ignoring: %m", dent->d_name); + else + cleanup_dir(subdir, mask, depth-1); + + (void) unlinkat(dirfd(dir), dent->d_name, AT_REMOVEDIR); + } else + (void) unlinkat(dirfd(dir), dent->d_name, 0); + } +} + +/* + * Assume that dir is a directory with file names matching udev data base + * entries for devices in /run/udev/data (such as "b8:16"), and removes + * all files except those that haven't been deleted in /run/udev/data + * (i.e. they were skipped during db cleanup because of the db_persist flag). + */ +static void cleanup_dir_after_db_cleanup(DIR *dir, DIR *datadir) { + assert(dir); + assert(datadir); + + FOREACH_DIRENT_ALL(dent, dir, break) { + if (dot_or_dot_dot(dent->d_name)) + continue; + + if (faccessat(dirfd(datadir), dent->d_name, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) + /* The corresponding udev database file still exists. + * Assuming the persistent flag is set for the database. */ + continue; + + (void) unlinkat(dirfd(dir), dent->d_name, 0); + } +} + +static void cleanup_dirs_after_db_cleanup(DIR *dir, DIR *datadir) { + assert(dir); + assert(datadir); + + FOREACH_DIRENT_ALL(dent, dir, break) { + struct stat stats; + + if (dot_or_dot_dot(dent->d_name)) + continue; + if (fstatat(dirfd(dir), dent->d_name, &stats, AT_SYMLINK_NOFOLLOW) < 0) + continue; + if (S_ISDIR(stats.st_mode)) { + _cleanup_closedir_ DIR *subdir = NULL; + + subdir = xopendirat(dirfd(dir), dent->d_name, O_NOFOLLOW); + if (!subdir) + log_debug_errno(errno, "Failed to open subdirectory '%s', ignoring: %m", dent->d_name); + else + cleanup_dir_after_db_cleanup(subdir, datadir); + + (void) unlinkat(dirfd(dir), dent->d_name, AT_REMOVEDIR); + } else + (void) unlinkat(dirfd(dir), dent->d_name, 0); + } +} + +static void cleanup_db(void) { + _cleanup_closedir_ DIR *dir1 = NULL, *dir2 = NULL, *dir3 = NULL, *dir4 = NULL; + + dir1 = opendir("/run/udev/data"); + if (dir1) + cleanup_dir(dir1, S_ISVTX, 1); + + dir2 = opendir("/run/udev/links"); + if (dir2) + cleanup_dirs_after_db_cleanup(dir2, dir1); + + dir3 = opendir("/run/udev/tags"); + if (dir3) + cleanup_dirs_after_db_cleanup(dir3, dir1); + + dir4 = opendir("/run/udev/static_node-tags"); + if (dir4) + cleanup_dir(dir4, 0, 2); + + /* Do not remove /run/udev/watch. It will be handled by udevd well on restart. + * And should not be removed by external program when udevd is running. */ +} + +static int query_device(QueryType query, sd_device* device) { + int r; + + assert(device); + + switch (query) { + case QUERY_NAME: { + const char *node; + + r = sd_device_get_devname(device, &node); + if (r < 0) + return log_error_errno(r, "No device node found: %m"); + + if (!arg_root) + assert_se(node = path_startswith(node, "/dev/")); + printf("%s\n", node); + return 0; + } + + case QUERY_SYMLINK: { + const char *prefix = ""; + + FOREACH_DEVICE_DEVLINK(device, devlink) { + if (!arg_root) + assert_se(devlink = path_startswith(devlink, "/dev/")); + printf("%s%s", prefix, devlink); + prefix = " "; + } + puts(""); + return 0; + } + + case QUERY_PATH: { + const char *devpath; + + r = sd_device_get_devpath(device, &devpath); + if (r < 0) + return log_error_errno(r, "Failed to get device path: %m"); + + printf("%s\n", devpath); + return 0; + } + + case QUERY_PROPERTY: + FOREACH_DEVICE_PROPERTY(device, key, value) { + if (arg_properties && !strv_contains(arg_properties, key)) + continue; + + if (arg_export) + printf("%s%s='%s'\n", strempty(arg_export_prefix), key, value); + else if (arg_value) + printf("%s\n", value); + else + printf("%s=%s\n", key, value); + } + + return 0; + + case QUERY_ALL: + if (arg_json_format_flags & JSON_FORMAT_OFF) + return print_record(device, NULL); + else { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + r = record_to_json(device, &v); + if (r < 0) + return r; + + (void) json_variant_dump(v, arg_json_format_flags, stdout, NULL); + } + + return 0; + + default: + assert_not_reached(); + } +} + +static int help(void) { + printf("%s info [OPTIONS] [DEVPATH|FILE]\n\n" + "Query sysfs or the udev database.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -q --query=TYPE Query device information:\n" + " name Name of device node\n" + " symlink Pointing to node\n" + " path sysfs device path\n" + " property The device properties\n" + " all All values\n" + " --property=NAME Show only properties by this name\n" + " --value When showing properties, print only their values\n" + " -p --path=SYSPATH sysfs device path used for query or attribute walk\n" + " -n --name=NAME Node or symlink name used for query or attribute walk\n" + " -r --root Prepend dev directory to path names\n" + " -a --attribute-walk Print all key matches walking along the chain\n" + " of parent devices\n" + " -t --tree Show tree of devices\n" + " -d --device-id-of-file=FILE Print major:minor of device containing this file\n" + " -x --export Export key/value pairs\n" + " -P --export-prefix Export the key name with a prefix\n" + " -e --export-db Export the content of the udev database\n" + " -c --cleanup-db Clean up the udev database\n" + " -w --wait-for-initialization[=SECONDS]\n" + " Wait for device to be initialized\n" + " --no-pager Do not pipe output into a pager\n" + " --json=pretty|short|off Generate JSON output\n" + " --subsystem-match=SUBSYSTEM\n" + " Query devices matching a subsystem\n" + " --subsystem-nomatch=SUBSYSTEM\n" + " Query devices not matching a subsystem\n" + " --attr-match=FILE[=VALUE]\n" + " Query devices that match an attribute\n" + " --attr-nomatch=FILE[=VALUE]\n" + " Query devices that do not match an attribute\n" + " --property-match=KEY=VALUE\n" + " Query devices with matching properties\n" + " --tag-match=TAG Query devices with a matching tag\n" + " --sysname-match=NAME Query devices with this /sys path\n" + " --name-match=NAME Query devices with this /dev name\n" + " --parent-match=NAME Query devices with this parent device\n" + " --initialized-match Query devices that are already initialized\n" + " --initialized-nomatch Query devices that are not initialized yet\n", + program_invocation_short_name); + + return 0; +} + +static int draw_tree( + sd_device *parent, + sd_device *const array[], size_t n, + const char *prefix, + unsigned level); + +static int output_tree_device( + sd_device *device, + const char *str, + const char *prefix, + bool more, + sd_device *const array[], size_t n, + unsigned level) { + + _cleanup_free_ char *subprefix = NULL, *subsubprefix = NULL; + + assert(device); + assert(str); + + prefix = strempty(prefix); + + printf("%s%s%s\n", prefix, special_glyph(more ? SPECIAL_GLYPH_TREE_BRANCH : SPECIAL_GLYPH_TREE_RIGHT), str); + + subprefix = strjoin(prefix, special_glyph(more ? SPECIAL_GLYPH_TREE_VERTICAL : SPECIAL_GLYPH_TREE_SPACE)); + if (!subprefix) + return log_oom(); + + subsubprefix = strjoin(subprefix, special_glyph(SPECIAL_GLYPH_VERTICAL_DOTTED), " "); + if (!subsubprefix) + return log_oom(); + + (void) print_record(device, subsubprefix); + + return draw_tree(device, array, n, subprefix, level + 1); +} + +static int draw_tree( + sd_device *parent, + sd_device *const array[], size_t n, + const char *prefix, + unsigned level) { + + const char *parent_path; + size_t i = 0; + int r; + + if (n == 0) + return 0; + + assert(array); + + if (parent) { + r = sd_device_get_devpath(parent, &parent_path); + if (r < 0) + return log_error_errno(r, "Failed to get sysfs path of parent device: %m"); + } else + parent_path = NULL; + + if (level > TREE_DEPTH_MAX) { + log_warning("Eliding tree below '%s', too deep.", strna(parent_path)); + return 0; + } + + while (i < n) { + sd_device *device = array[i]; + const char *device_path, *str; + bool more = false; + size_t j; + + r = sd_device_get_devpath(device, &device_path); + if (r < 0) + return log_error_errno(r, "Failed to get sysfs path of enumerated device: %m"); + + /* Scan through the subsequent devices looking children of the device we are looking at. */ + for (j = i + 1; j < n; j++) { + sd_device *next = array[j]; + const char *next_path; + + r = sd_device_get_devpath(next, &next_path); + if (r < 0) + return log_error_errno(r, "Failed to get sysfs of child device: %m"); + + if (!path_startswith(next_path, device_path)) { + more = !parent_path || path_startswith(next_path, parent_path); + break; + } + } + + /* Determine the string to display for this node. If we are at the top of the tree, the full + * device path so far, otherwise just the part suffixing the parent's device path. */ + str = parent ? ASSERT_PTR(path_startswith(device_path, parent_path)) : device_path; + + r = output_tree_device(device, str, prefix, more, array + i + 1, j - i - 1, level); + if (r < 0) + return r; + + i = j; + } + + return 0; +} + +static int print_tree(sd_device* below) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + const char *below_path; + sd_device **array; + size_t n = 0; + int r; + + if (below) { + r = sd_device_get_devpath(below, &below_path); + if (r < 0) + return log_error_errno(r, "Failed to get sysfs path of device: %m"); + + } else + below_path = NULL; + + r = sd_device_enumerator_new(&e); + if (r < 0) + return log_error_errno(r, "Failed to allocate device enumerator: %m"); + + if (below) { + r = sd_device_enumerator_add_match_parent(e, below); + if (r < 0) + return log_error_errno(r, "Failed to install parent enumerator match: %m"); + } + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return log_error_errno(r, "Failed to enable enumeration of uninitialized devices: %m"); + + r = device_enumerator_scan_devices_and_subsystems(e); + if (r < 0) + return log_error_errno(r, "Failed to scan for devices and subsystems: %m"); + + if (below) { + /* This must be called after device_enumerator_scan_devices_and_subsystems(). */ + r = device_enumerator_add_parent_devices(e, below); + if (r < 0) + return log_error_errno(r, "Failed to add parent devices: %m"); + } + + assert_se(array = device_enumerator_get_devices(e, &n)); + + if (n == 0) { + log_info("No items."); + return 0; + } + + r = draw_tree(NULL, array, n, NULL, 0); + if (r < 0) + return r; + + printf("\n%zu items shown.\n", n); + return 0; +} + +static int ensure_device_enumerator(sd_device_enumerator **e) { + int r; + + assert(e); + + if (*e) + return 0; + + r = sd_device_enumerator_new(e); + if (r < 0) + return log_error_errno(r, "Failed to create device enumerator: %m"); + + r = sd_device_enumerator_allow_uninitialized(*e); + if (r < 0) + return log_error_errno(r, "Failed to allow uninitialized devices: %m"); + + return 0; +} + +static int parse_key_value_argument(const char *s, char **key, char **value) { + _cleanup_free_ char *k = NULL, *v = NULL; + int r; + + assert(s); + assert(key); + assert(value); + + r = extract_many_words(&s, "=", EXTRACT_DONT_COALESCE_SEPARATORS, &k, &v, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse key/value pair %s: %m", s); + if (r < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing '=' in key/value pair %s.", s); + + if (!filename_is_valid(k)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "%s is not a valid key name", k); + + free_and_replace(*key, k); + free_and_replace(*value, v); + return 0; +} + +int info_main(int argc, char *argv[], void *userdata) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + _cleanup_strv_free_ char **devices = NULL; + _cleanup_free_ char *name = NULL; + int c, r, ret; + + enum { + ARG_PROPERTY = 0x100, + ARG_VALUE, + ARG_NO_PAGER, + ARG_JSON, + ARG_SUBSYSTEM_MATCH, + ARG_SUBSYSTEM_NOMATCH, + ARG_ATTR_MATCH, + ARG_ATTR_NOMATCH, + ARG_PROPERTY_MATCH, + ARG_TAG_MATCH, + ARG_SYSNAME_MATCH, + ARG_NAME_MATCH, + ARG_PARENT_MATCH, + ARG_INITIALIZED_MATCH, + ARG_INITIALIZED_NOMATCH, + }; + + static const struct option options[] = { + { "attribute-walk", no_argument, NULL, 'a' }, + { "tree", no_argument, NULL, 't' }, + { "cleanup-db", no_argument, NULL, 'c' }, + { "device-id-of-file", required_argument, NULL, 'd' }, + { "export", no_argument, NULL, 'x' }, + { "export-db", no_argument, NULL, 'e' }, + { "export-prefix", required_argument, NULL, 'P' }, + { "help", no_argument, NULL, 'h' }, + { "name", required_argument, NULL, 'n' }, + { "path", required_argument, NULL, 'p' }, + { "property", required_argument, NULL, ARG_PROPERTY }, + { "query", required_argument, NULL, 'q' }, + { "root", no_argument, NULL, 'r' }, + { "value", no_argument, NULL, ARG_VALUE }, + { "version", no_argument, NULL, 'V' }, + { "wait-for-initialization", optional_argument, NULL, 'w' }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "json", required_argument, NULL, ARG_JSON }, + { "subsystem-match", required_argument, NULL, ARG_SUBSYSTEM_MATCH }, + { "subsystem-nomatch", required_argument, NULL, ARG_SUBSYSTEM_NOMATCH }, + { "attr-match", required_argument, NULL, ARG_ATTR_MATCH }, + { "attr-nomatch", required_argument, NULL, ARG_ATTR_NOMATCH }, + { "property-match", required_argument, NULL, ARG_PROPERTY_MATCH }, + { "tag-match", required_argument, NULL, ARG_TAG_MATCH }, + { "sysname-match", required_argument, NULL, ARG_SYSNAME_MATCH }, + { "name-match", required_argument, NULL, ARG_NAME_MATCH }, + { "parent-match", required_argument, NULL, ARG_PARENT_MATCH }, + { "initialized-match", no_argument, NULL, ARG_INITIALIZED_MATCH }, + { "initialized-nomatch", no_argument, NULL, ARG_INITIALIZED_NOMATCH }, + {} + }; + + ActionType action = ACTION_QUERY; + QueryType query = QUERY_ALL; + + while ((c = getopt_long(argc, argv, "atced:n:p:q:rxP:w::Vh", options, NULL)) >= 0) + switch (c) { + case ARG_PROPERTY: + /* Make sure that if the empty property list was specified, we won't show any + properties. */ + if (isempty(optarg) && !arg_properties) { + arg_properties = new0(char*, 1); + if (!arg_properties) + return log_oom(); + } else { + r = strv_split_and_extend(&arg_properties, optarg, ",", true); + if (r < 0) + return log_oom(); + } + break; + case ARG_VALUE: + arg_value = true; + break; + case 'n': + case 'p': { + const char *prefix = c == 'n' ? "/dev/" : "/sys/"; + char *path; + + path = path_join(path_startswith(optarg, prefix) ? NULL : prefix, optarg); + if (!path) + return log_oom(); + + r = strv_consume(&devices, path); + if (r < 0) + return log_oom(); + break; + } + + case 'q': + action = ACTION_QUERY; + if (streq(optarg, "property") || streq(optarg, "env")) + query = QUERY_PROPERTY; + else if (streq(optarg, "name")) + query = QUERY_NAME; + else if (streq(optarg, "symlink")) + query = QUERY_SYMLINK; + else if (streq(optarg, "path")) + query = QUERY_PATH; + else if (streq(optarg, "all")) + query = QUERY_ALL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "unknown query type"); + break; + case 'r': + arg_root = true; + break; + case 'd': + action = ACTION_DEVICE_ID_FILE; + r = free_and_strdup(&name, optarg); + if (r < 0) + return log_oom(); + break; + case 'a': + action = ACTION_ATTRIBUTE_WALK; + break; + case 't': + action = ACTION_TREE; + break; + case 'e': + action = ACTION_EXPORT; + break; + case 'c': + cleanup_db(); + return 0; + case 'x': + arg_export = true; + break; + case 'P': + arg_export = true; + arg_export_prefix = optarg; + break; + case 'w': + if (optarg) { + r = parse_sec(optarg, &arg_wait_for_initialization_timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value: %m"); + } else + arg_wait_for_initialization_timeout = USEC_INFINITY; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + break; + + case ARG_SUBSYSTEM_MATCH: + case ARG_SUBSYSTEM_NOMATCH: + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, optarg, c == ARG_SUBSYSTEM_MATCH); + if (r < 0) + return log_error_errno(r, "Failed to add%s subsystem match '%s': %m", + c == ARG_SUBSYSTEM_MATCH ? "" : " negative", optarg); + + break; + + case ARG_ATTR_MATCH: + case ARG_ATTR_NOMATCH: { + _cleanup_free_ char *k = NULL, *v = NULL; + + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = parse_key_value_argument(optarg, &k, &v); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_sysattr(e, k, v, c == ARG_ATTR_MATCH); + if (r < 0) + return log_error_errno(r, "Failed to add%s sysattr match '%s=%s': %m", + c == ARG_ATTR_MATCH ? "" : " negative", k, v); + break; + } + + case ARG_PROPERTY_MATCH: { + _cleanup_free_ char *k = NULL, *v = NULL; + + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = parse_key_value_argument(optarg, &k, &v); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_property_required(e, k, v); + if (r < 0) + return log_error_errno(r, "Failed to add property match '%s=%s': %m", k, v); + break; + } + + case ARG_TAG_MATCH: + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_tag(e, optarg); + if (r < 0) + return log_error_errno(r, "Failed to add tag match '%s': %m", optarg); + break; + + case ARG_SYSNAME_MATCH: + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_sysname(e, optarg); + if (r < 0) + return log_error_errno(r, "Failed to add sysname match '%s': %m", optarg); + break; + + case ARG_NAME_MATCH: + case ARG_PARENT_MATCH: { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(optarg, c == ARG_NAME_MATCH ? "/dev" : "/sys", &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", optarg); + + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", optarg); + break; + } + + case ARG_INITIALIZED_MATCH: + case ARG_INITIALIZED_NOMATCH: + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + r = device_enumerator_add_match_is_initialized(e, c == ARG_INITIALIZED_MATCH ? MATCH_INITIALIZED_YES : MATCH_INITIALIZED_NO); + if (r < 0) + return log_error_errno(r, "Failed to set initialized filter: %m"); + break; + + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (action == ACTION_DEVICE_ID_FILE) { + if (argv[optind]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Positional arguments are not allowed with -d/--device-id-of-file."); + assert(name); + return stat_device(name, arg_export, arg_export_prefix); + } + + if (action == ACTION_EXPORT) { + r = ensure_device_enumerator(&e); + if (r < 0) + return r; + + return export_devices(e); + } + + r = strv_extend_strv(&devices, argv + optind, false); + if (r < 0) + return log_error_errno(r, "Failed to build argument list: %m"); + + if (action != ACTION_TREE && strv_isempty(devices)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "A device name or path is required"); + if (IN_SET(action, ACTION_ATTRIBUTE_WALK, ACTION_TREE) && strv_length(devices) > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Only one device may be specified with -a/--attribute-walk and -t/--tree"); + + if (arg_export && arg_value) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "-x/--export or -P/--export-prefix cannot be used with --value"); + + pager_open(arg_pager_flags); + + if (strv_isempty(devices)) { + assert(action == ACTION_TREE); + return print_tree(NULL); + } + + ret = 0; + STRV_FOREACH(p, devices) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + + r = find_device(*p, NULL, &device); + if (r < 0) { + if (r == -EINVAL) + log_error_errno(r, "Bad argument \"%s\", expected an absolute path in /dev/ or /sys/ or a unit name: %m", *p); + else + log_error_errno(r, "Unknown device \"%s\": %m", *p); + + if (ret == 0) + ret = r; + continue; + } + + if (arg_wait_for_initialization_timeout > 0) { + sd_device *d; + + r = device_wait_for_initialization( + device, + NULL, + arg_wait_for_initialization_timeout, + &d); + if (r < 0) + return r; + + sd_device_unref(device); + device = d; + } + + if (action == ACTION_QUERY) + r = query_device(query, device); + else if (action == ACTION_ATTRIBUTE_WALK) + r = print_device_chain(device); + else if (action == ACTION_TREE) + r = print_tree(device); + else + assert_not_reached(); + if (r < 0) + return r; + } + + return ret; +} diff --git a/src/udev/udevadm-lock.c b/src/udev/udevadm-lock.c new file mode 100644 index 0000000..bc2d5e7 --- /dev/null +++ b/src/udev/udevadm-lock.c @@ -0,0 +1,306 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <getopt.h> +#include <stdlib.h> +#include <sys/file.h> +#include <unistd.h> + +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "device-util.h" +#include "fd-util.h" +#include "fdset.h" +#include "lock-util.h" +#include "main-func.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "process-util.h" +#include "signal-util.h" +#include "sort-util.h" +#include "strv.h" +#include "time-util.h" +#include "udevadm.h" + +static usec_t arg_timeout_usec = USEC_INFINITY; +static char **arg_devices = NULL; +static char **arg_backing = NULL; +static char **arg_cmdline = NULL; +static bool arg_print = false; + +STATIC_DESTRUCTOR_REGISTER(arg_devices, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_backing, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("udevadm", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] COMMAND\n" + "%s [OPTIONS...] --print\n" + "\n%sLock a block device and run a command.%s\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -d --device=DEVICE Block device to lock\n" + " -b --backing=FILE File whose backing block device to lock\n" + " -t --timeout=SECS Block at most the specified time waiting for lock\n" + " -p --print Only show which block device the lock would be taken on\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "device", required_argument, NULL, 'd' }, + { "backing", required_argument, NULL, 'b' }, + { "timeout", required_argument, NULL, 't' }, + { "print", no_argument, NULL, 'p' }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long() + * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */ + optind = 0; + while ((c = getopt_long(argc, argv, arg_print ? "hVd:b:t:p" : "+hVd:b:t:p", options, NULL)) >= 0) + + switch (c) { + + case 'h': + return help(); + + case 'V': + return print_version(); + + case 'd': + case 'b': { + _cleanup_free_ char *s = NULL; + char ***l = c == 'd' ? &arg_devices : &arg_backing; + + r = path_make_absolute_cwd(optarg, &s); + if (r < 0) + return log_error_errno(r, "Failed to make path '%s' absolute: %m", optarg); + + path_simplify(s); + + if (strv_consume(l, TAKE_PTR(s)) < 0) + return log_oom(); + + strv_uniq(*l); + break; + } + + case 't': + r = parse_sec(optarg, &arg_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to parse --timeout= parameter: %s", optarg); + break; + + case 'p': + arg_print = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_print) { + if (optind != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No arguments expected"); + } else { + if (optind + 1 > argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments, command to execute."); + + arg_cmdline = strv_copy(argv + optind); + if (!arg_cmdline) + return log_oom(); + } + + if (strv_isempty(arg_devices) && strv_isempty(arg_backing)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No devices to lock specified, refusing."); + + return 1; +} + +static int find_devno( + dev_t **devnos, + size_t *n_devnos, + const char *device, + bool backing) { + + dev_t devt; + int r; + + assert(devnos); + assert(n_devnos); + assert(*devnos || *n_devnos == 0); + assert(device); + + r = path_get_whole_disk(device, backing, &devt); + if (r < 0) + return log_error_errno(r, "Failed to find whole block device for '%s': %m", device); + + if (typesafe_bsearch(&devt, *devnos, *n_devnos, devt_compare_func)) { + log_debug("Device %u:%u already listed for locking, ignoring.", major(devt), minor(devt)); + return 0; + } + + if (!GREEDY_REALLOC(*devnos, *n_devnos + 1)) + return log_oom(); + + (*devnos)[(*n_devnos)++] = devt; + + /* Immediately sort again, to ensure the binary search above will work for the next device we add */ + typesafe_qsort(*devnos, *n_devnos, devt_compare_func); + return 1; +} + +static int lock_device( + const char *path, + dev_t devno, + usec_t deadline) { + + _cleanup_close_ int fd = -EBADF; + struct stat st; + int r; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", path); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat '%s': %m", path); + + /* Extra safety: check that the device still refers to what we think it refers to */ + if (!S_ISBLK(st.st_mode) || st.st_rdev != devno) + return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Path '%s' no longer refers to specified block device %u:%u: %m", path, major(devno), minor(devno)); + + r = lock_generic(fd, LOCK_BSD, LOCK_EX|LOCK_NB); + if (r < 0) { + if (r != -EAGAIN) + return log_error_errno(r, "Failed to lock device '%s': %m", path); + + if (deadline == 0) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Device '%s' is currently locked.", path); + + if (deadline == USEC_INFINITY) { + log_info("Device '%s' is currently locked, waiting%s", path, special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + r = lock_generic(fd, LOCK_BSD, LOCK_EX); + } else { + usec_t left = usec_sub_unsigned(deadline, now(CLOCK_MONOTONIC)); + + log_info("Device '%s' is currently locked, waiting %s%s", + path, FORMAT_TIMESPAN(left, 0), + special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + r = lock_generic_with_timeout(fd, LOCK_BSD, LOCK_EX, left); + if (r == -ETIMEDOUT) + return log_error_errno(r, "Timeout reached."); + } + if (r < 0) + return log_error_errno(r, "Failed to lock device '%s': %m", path); + } + + log_debug("Successfully locked %s (%u:%u)%s", path, major(devno), minor(devno), special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + return TAKE_FD(fd); +} + +int lock_main(int argc, char *argv[], void *userdata) { + _cleanup_fdset_free_ FDSet *fds = NULL; + _cleanup_free_ dev_t *devnos = NULL; + size_t n_devnos = 0; + usec_t deadline; + pid_t pid; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + STRV_FOREACH(i, arg_devices) { + r = find_devno(&devnos, &n_devnos, *i, /* backing= */ false); + if (r < 0) + return r; + } + + STRV_FOREACH(i, arg_backing) { + r = find_devno(&devnos, &n_devnos, *i, /* backing= */ true); + if (r < 0) + return r; + } + + assert(n_devnos > 0); + + fds = fdset_new(); + if (!fds) + return log_oom(); + + if (!timestamp_is_set(arg_timeout_usec)) + deadline = arg_timeout_usec; + else + deadline = usec_add(now(CLOCK_MONOTONIC), arg_timeout_usec); + + for (size_t i = 0; i < n_devnos; i++) { + _cleanup_free_ char *node = NULL; + + r = devname_from_devnum(S_IFBLK, devnos[i], &node); + if (r < 0) + return log_error_errno(r, "Failed to format block device path: %m"); + + if (arg_print) + printf("%s\n", node); + else { + _cleanup_close_ int fd = -EBADF; + + fd = lock_device(node, devnos[i], deadline); + if (fd < 0) + return fd; + + r = fdset_consume(fds, TAKE_FD(fd)); + if (r < 0) + return log_oom(); + } + } + + if (arg_print) + return EXIT_SUCCESS; + + /* Ignore SIGINT and allow the forked process to receive it */ + (void) ignore_signals(SIGINT); + + r = safe_fork("(lock)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_CLOSE_ALL_FDS|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + execvp(arg_cmdline[0], arg_cmdline); + log_open(); + log_error_errno(errno, "Failed to execute %s: %m", arg_cmdline[0]); + _exit(EXIT_FAILURE); + } + + return wait_for_terminate_and_check(arg_cmdline[0], pid, 0); +} diff --git a/src/udev/udevadm-monitor.c b/src/udev/udevadm-monitor.c new file mode 100644 index 0000000..27c4853 --- /dev/null +++ b/src/udev/udevadm-monitor.c @@ -0,0 +1,246 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "device-monitor-private.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "hashmap.h" +#include "set.h" +#include "signal-util.h" +#include "string-util.h" +#include "udevadm.h" +#include "virt.h" +#include "time-util.h" + +static bool arg_show_property = false; +static bool arg_print_kernel = false; +static bool arg_print_udev = false; +static Set *arg_tag_filter = NULL; +static Hashmap *arg_subsystem_filter = NULL; + +static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) { + sd_device_action_t action = _SD_DEVICE_ACTION_INVALID; + const char *devpath = NULL, *subsystem = NULL; + MonitorNetlinkGroup group = PTR_TO_INT(userdata); + struct timespec ts; + + assert(device); + assert(IN_SET(group, MONITOR_GROUP_UDEV, MONITOR_GROUP_KERNEL)); + + (void) sd_device_get_action(device, &action); + (void) sd_device_get_devpath(device, &devpath); + (void) sd_device_get_subsystem(device, &subsystem); + + assert_se(clock_gettime(CLOCK_MONOTONIC, &ts) == 0); + + printf("%-6s[%"PRI_TIME".%06"PRI_NSEC"] %-8s %s (%s)\n", + group == MONITOR_GROUP_UDEV ? "UDEV" : "KERNEL", + ts.tv_sec, (nsec_t)ts.tv_nsec/1000, + strna(device_action_to_string(action)), + devpath, subsystem); + + if (arg_show_property) { + FOREACH_DEVICE_PROPERTY(device, key, value) + printf("%s=%s\n", key, value); + + printf("\n"); + } + + return 0; +} + +static int setup_monitor(MonitorNetlinkGroup sender, sd_event *event, sd_device_monitor **ret) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + const char *subsystem, *devtype, *tag; + int r; + + r = device_monitor_new_full(&monitor, sender, -1); + if (r < 0) + return log_error_errno(r, "Failed to create netlink socket: %m"); + + r = sd_device_monitor_attach_event(monitor, event); + if (r < 0) + return log_error_errno(r, "Failed to attach event: %m"); + + HASHMAP_FOREACH_KEY(devtype, subsystem, arg_subsystem_filter) { + r = sd_device_monitor_filter_add_match_subsystem_devtype(monitor, subsystem, devtype); + if (r < 0) + return log_error_errno(r, "Failed to apply subsystem filter '%s%s%s': %m", + subsystem, devtype ? "/" : "", strempty(devtype)); + } + + SET_FOREACH(tag, arg_tag_filter) { + r = sd_device_monitor_filter_add_match_tag(monitor, tag); + if (r < 0) + return log_error_errno(r, "Failed to apply tag filter '%s': %m", tag); + } + + r = sd_device_monitor_start(monitor, device_monitor_handler, INT_TO_PTR(sender)); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + (void) sd_device_monitor_set_description(monitor, sender == MONITOR_GROUP_UDEV ? "udev" : "kernel"); + + *ret = TAKE_PTR(monitor); + return 0; +} + +static int help(void) { + printf("%s monitor [OPTIONS]\n\n" + "Listen to kernel and udev events.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -p --property Print the event properties\n" + " -k --kernel Print kernel uevents\n" + " -u --udev Print udev events\n" + " -s --subsystem-match=SUBSYSTEM[/DEVTYPE] Filter events by subsystem\n" + " -t --tag-match=TAG Filter events by tag\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "property", no_argument, NULL, 'p' }, + { "environment", no_argument, NULL, 'e' }, /* alias for -p */ + { "kernel", no_argument, NULL, 'k' }, + { "udev", no_argument, NULL, 'u' }, + { "subsystem-match", required_argument, NULL, 's' }, + { "tag-match", required_argument, NULL, 't' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int r, c; + + while ((c = getopt_long(argc, argv, "pekus:t:Vh", options, NULL)) >= 0) + switch (c) { + case 'p': + case 'e': + arg_show_property = true; + break; + case 'k': + arg_print_kernel = true; + break; + case 'u': + arg_print_udev = true; + break; + case 's': { + _cleanup_free_ char *subsystem = NULL, *devtype = NULL; + const char *slash; + + slash = strchr(optarg, '/'); + if (slash) { + devtype = strdup(slash + 1); + if (!devtype) + return -ENOMEM; + + subsystem = strndup(optarg, slash - optarg); + } else + subsystem = strdup(optarg); + + if (!subsystem) + return -ENOMEM; + + r = hashmap_ensure_put(&arg_subsystem_filter, NULL, subsystem, devtype); + if (r < 0) + return r; + + TAKE_PTR(subsystem); + TAKE_PTR(devtype); + break; + } + case 't': + /* optarg is stored in argv[], so we don't need to copy it */ + r = set_ensure_put(&arg_tag_filter, &string_hash_ops, optarg); + if (r < 0) + return r; + break; + + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (!arg_print_kernel && !arg_print_udev) { + arg_print_kernel = true; + arg_print_udev = true; + } + + return 1; +} + +int monitor_main(int argc, char *argv[], void *userdata) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *kernel_monitor = NULL, *udev_monitor = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + goto finalize; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + /* Callers are expecting to see events as they happen: Line buffering */ + setlinebuf(stdout); + + r = sd_event_default(&event); + if (r < 0) { + log_error_errno(r, "Failed to initialize event: %m"); + goto finalize; + } + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0); + (void) sd_event_add_signal(event, NULL, SIGTERM, NULL, NULL); + (void) sd_event_add_signal(event, NULL, SIGINT, NULL, NULL); + + printf("monitor will print the received events for:\n"); + if (arg_print_udev) { + r = setup_monitor(MONITOR_GROUP_UDEV, event, &udev_monitor); + if (r < 0) + goto finalize; + + printf("UDEV - the event which udev sends out after rule processing\n"); + } + + if (arg_print_kernel) { + r = setup_monitor(MONITOR_GROUP_KERNEL, event, &kernel_monitor); + if (r < 0) + goto finalize; + + printf("KERNEL - the kernel uevent\n"); + } + printf("\n"); + + r = sd_event_loop(event); + if (r < 0) { + log_error_errno(r, "Failed to run event loop: %m"); + goto finalize; + } + + r = 0; + +finalize: + hashmap_free_free_free(arg_subsystem_filter); + set_free(arg_tag_filter); + + return r; +} diff --git a/src/udev/udevadm-settle.c b/src/udev/udevadm-settle.c new file mode 100644 index 0000000..c236a70 --- /dev/null +++ b/src/udev/udevadm-settle.c @@ -0,0 +1,252 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2009 Canonical Ltd. + * Copyright © 2009 Scott James Remnant <scott@netsplit.com> + */ + +#include <getopt.h> +#include <unistd.h> + +#include "sd-bus.h" +#include "sd-event.h" +#include "sd-login.h" +#include "sd-messages.h" + +#include "bus-util.h" +#include "path-util.h" +#include "strv.h" +#include "time-util.h" +#include "udev-ctrl.h" +#include "udev-util.h" +#include "udevadm.h" +#include "unit-def.h" +#include "virt.h" + +static usec_t arg_timeout_usec = 120 * USEC_PER_SEC; +static const char *arg_exists = NULL; + +static int help(void) { + printf("%s settle [OPTIONS]\n\n" + "Wait for pending udev events.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -t --timeout=SEC Maximum time to wait for events\n" + " -E --exit-if-exists=FILE Stop waiting if file exists\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "timeout", required_argument, NULL, 't' }, + { "exit-if-exists", required_argument, NULL, 'E' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + { "seq-start", required_argument, NULL, 's' }, /* removed */ + { "seq-end", required_argument, NULL, 'e' }, /* removed */ + { "quiet", no_argument, NULL, 'q' }, /* removed */ + {} + }; + + int c, r; + + while ((c = getopt_long(argc, argv, "t:E:Vhs:e:q", options, NULL)) >= 0) { + switch (c) { + case 't': + r = parse_sec(optarg, &arg_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg); + break; + case 'E': + if (!path_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path: %s", optarg); + + arg_exists = optarg; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case 's': + case 'e': + case 'q': + return log_info_errno(SYNTHETIC_ERRNO(EINVAL), + "Option -%c no longer supported.", + c); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + } + + return 1; +} + +static int emit_deprecation_warning(void) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **a = NULL; + _cleanup_free_ char *unit = NULL; + int r; + + r = sd_pid_get_unit(0, &unit); + if (r < 0) { + log_debug_errno(r, "Failed to determine unit we run in, ignoring: %m"); + return 0; + } + + if (!streq(unit, "systemd-udev-settle.service")) + return 0; + + r = bus_connect_system_systemd(&bus); + if (r < 0) + log_debug_errno(r, "Failed to open connection to systemd, skipping dependency queries: %m"); + else { + _cleanup_strv_free_ char **b = NULL; + _cleanup_free_ char *unit_path = NULL; + + unit_path = unit_dbus_path_from_name("systemd-udev-settle.service"); + if (!unit_path) + return -ENOMEM; + + (void) sd_bus_get_property_strv( + bus, + "org.freedesktop.systemd1", + unit_path, + "org.freedesktop.systemd1.Unit", + "WantedBy", + NULL, + &a); + + (void) sd_bus_get_property_strv( + bus, + "org.freedesktop.systemd1", + unit_path, + "org.freedesktop.systemd1.Unit", + "RequiredBy", + NULL, + &b); + + r = strv_extend_strv(&a, b, true); + if (r < 0) + return r; + } + + if (strv_isempty(a)) + /* Print a simple message if we cannot determine the dependencies */ + log_notice("systemd-udev-settle.service is deprecated."); + else { + /* Print a longer, structured message if we can acquire the dependencies (this should be the + * common case). This is hooked up with a catalog entry and everything. */ + _cleanup_free_ char *t = NULL; + + t = strv_join(a, ", "); + if (!t) + return -ENOMEM; + + log_struct(LOG_NOTICE, + LOG_MESSAGE("systemd-udev-settle.service is deprecated. Please fix %s not to pull it in.", t), + "OFFENDING_UNITS=%s", t, + "MESSAGE_ID=" SD_MESSAGE_SYSTEMD_UDEV_SETTLE_DEPRECATED_STR); + } + + return 0; +} + +static bool check(void) { + int r; + + if (arg_exists) { + if (access(arg_exists, F_OK) >= 0) + return true; + + if (errno != ENOENT) + log_warning_errno(errno, "Failed to check the existence of \"%s\", ignoring: %m", arg_exists); + } + + /* exit if queue is empty */ + r = udev_queue_is_empty(); + if (r < 0) + log_warning_errno(r, "Failed to check if udev queue is empty, ignoring: %m"); + + return r > 0; +} + +static int on_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) { + assert(s); + + if (check()) + return sd_event_exit(sd_event_source_get_event(s), 0); + + return 0; +} + +int settle_main(int argc, char *argv[], void *userdata) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + (void) emit_deprecation_warning(); + + if (getuid() == 0) { + _cleanup_(udev_ctrl_unrefp) UdevCtrl *uctrl = NULL; + + /* guarantee that the udev daemon isn't pre-processing */ + + r = udev_ctrl_new(&uctrl); + if (r < 0) + return log_error_errno(r, "Failed to create control socket for udev daemon: %m"); + + r = udev_ctrl_send_ping(uctrl); + if (r < 0) { + log_debug_errno(r, "Failed to connect to udev daemon, ignoring: %m"); + return 0; + } + + r = udev_ctrl_wait(uctrl, MAX(5 * USEC_PER_SEC, arg_timeout_usec)); + if (r < 0) + return log_error_errno(r, "Failed to wait for daemon to reply: %m"); + } else { + /* For non-privileged users, at least check if udevd is running. */ + if (access("/run/udev/control", F_OK) < 0) + return log_error_errno(errno, + errno == ENOENT ? "systemd-udevd is not running." : + "Failed to check if /run/udev/control exists: %m"); + } + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get default sd-event object: %m"); + + r = sd_event_add_inotify(event, NULL, "/run/udev" , IN_DELETE, on_inotify, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add inotify watch for /run/udev: %m"); + + if (arg_timeout_usec != USEC_INFINITY) { + r = sd_event_add_time_relative(event, NULL, CLOCK_BOOTTIME, arg_timeout_usec, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)); + if (r < 0) + return log_error_errno(r, "Failed to add timer event source: %m"); + } + + /* Check before entering the event loop, as the udev queue may be already empty. */ + if (check()) + return 0; + + r = sd_event_loop(event); + if (r == -ETIMEDOUT) + return log_error_errno(r, "Timed out for waiting the udev queue being empty."); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} diff --git a/src/udev/udevadm-test-builtin.c b/src/udev/udevadm-test-builtin.c new file mode 100644 index 0000000..088b4da --- /dev/null +++ b/src/udev/udevadm-test-builtin.c @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +#include "log.h" +#include "udev-builtin.h" +#include "udevadm.h" +#include "udevadm-util.h" + +static sd_device_action_t arg_action = SD_DEVICE_ADD; +static const char *arg_command = NULL; +static const char *arg_syspath = NULL; + +static int help(void) { + printf("%s test-builtin [OPTIONS] COMMAND DEVPATH\n\n" + "Test a built-in command.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n\n" + " -a --action=ACTION|help Set action string\n" + "Commands:\n", + program_invocation_short_name); + + udev_builtin_list(); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "action", required_argument, NULL, 'a' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int r, c; + + while ((c = getopt_long(argc, argv, "a:Vh", options, NULL)) >= 0) + switch (c) { + case 'a': + r = parse_device_action(optarg, &arg_action); + if (r < 0) + return log_error_errno(r, "Invalid action '%s'", optarg); + if (r == 0) + return 0; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + arg_command = argv[optind++]; + if (!arg_command) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Command missing."); + + arg_syspath = argv[optind++]; + if (!arg_syspath) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "device is missing."); + + return 1; +} + +int builtin_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_event_freep) UdevEvent *event = NULL; + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + UdevBuiltinCommand cmd; + int r; + + log_set_max_level(LOG_DEBUG); + log_parse_environment(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + udev_builtin_init(); + + cmd = udev_builtin_lookup(arg_command); + if (cmd < 0) { + r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command '%s'", arg_command); + goto finish; + } + + r = find_device_with_action(arg_syspath, arg_action, &dev); + if (r < 0) { + log_error_errno(r, "Failed to open device '%s': %m", arg_syspath); + goto finish; + } + + event = udev_event_new(dev, 0, NULL, LOG_DEBUG); + if (!event) { + r = log_oom(); + goto finish; + } + + r = udev_builtin_run(event, cmd, arg_command, true); + if (r < 0) { + log_debug_errno(r, "Builtin command '%s' fails: %m", arg_command); + goto finish; + } + + r = 0; +finish: + udev_builtin_exit(); + return r; +} diff --git a/src/udev/udevadm-test.c b/src/udev/udevadm-test.c new file mode 100644 index 0000000..e1afd7d --- /dev/null +++ b/src/udev/udevadm-test.c @@ -0,0 +1,152 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2003-2004 Greg Kroah-Hartman <greg@kroah.com> + */ + +#include <errno.h> +#include <getopt.h> +#include <signal.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/signalfd.h> +#include <unistd.h> + +#include "sd-device.h" + +#include "device-private.h" +#include "device-util.h" +#include "path-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" +#include "udev-event.h" +#include "udev-format.h" +#include "udevadm-util.h" +#include "udevadm.h" + +static sd_device_action_t arg_action = SD_DEVICE_ADD; +static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY; +static const char *arg_syspath = NULL; + +static int help(void) { + + printf("%s test [OPTIONS] DEVPATH\n\n" + "Test an event run.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -a --action=ACTION|help Set action string\n" + " -N --resolve-names=early|late|never When to resolve names\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "action", required_argument, NULL, 'a' }, + { "resolve-names", required_argument, NULL, 'N' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int r, c; + + while ((c = getopt_long(argc, argv, "a:N:Vh", options, NULL)) >= 0) + switch (c) { + case 'a': + r = parse_device_action(optarg, &arg_action); + if (r < 0) + return log_error_errno(r, "Invalid action '%s'", optarg); + if (r == 0) + return 0; + break; + case 'N': + arg_resolve_name_timing = resolve_name_timing_from_string(optarg); + if (arg_resolve_name_timing < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--resolve-names= must be early, late or never"); + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + arg_syspath = argv[optind]; + if (!arg_syspath) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "syspath parameter missing."); + + return 1; +} + +int test_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_(udev_event_freep) UdevEvent *event = NULL; + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + const char *cmd; + sigset_t mask, sigmask_orig; + void *val; + int r; + + log_set_max_level(LOG_DEBUG); + log_parse_environment(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + printf("This program is for debugging only, it does not run any program\n" + "specified by a RUN key. It may show incorrect results, because\n" + "some values may be different, or not available at a simulation run.\n" + "\n"); + + assert_se(sigprocmask(SIG_SETMASK, NULL, &sigmask_orig) >= 0); + + udev_builtin_init(); + + r = udev_rules_load(&rules, arg_resolve_name_timing); + if (r < 0) { + log_error_errno(r, "Failed to read udev rules: %m"); + goto out; + } + + r = find_device_with_action(arg_syspath, arg_action, &dev); + if (r < 0) { + log_error_errno(r, "Failed to open device '%s': %m", arg_syspath); + goto out; + } + + /* don't read info from the db */ + device_seal(dev); + + event = udev_event_new(dev, 0, NULL, LOG_DEBUG); + + assert_se(sigfillset(&mask) >= 0); + assert_se(sigprocmask(SIG_SETMASK, &mask, &sigmask_orig) >= 0); + + udev_event_execute_rules(event, -1, 60 * USEC_PER_SEC, SIGKILL, NULL, rules); + + FOREACH_DEVICE_PROPERTY(dev, key, value) + printf("%s=%s\n", key, value); + + ORDERED_HASHMAP_FOREACH_KEY(val, cmd, event->run_list) { + char program[UDEV_PATH_SIZE]; + bool truncated; + + (void) udev_event_apply_format(event, cmd, program, sizeof(program), false, NULL, &truncated); + if (truncated) + log_warning("The command '%s' is truncated while substituting into '%s'.", program, cmd); + printf("run: '%s'\n", program); + } + + r = 0; +out: + udev_builtin_exit(); + return r; +} diff --git a/src/udev/udevadm-trigger.c b/src/udev/udevadm-trigger.c new file mode 100644 index 0000000..e0f487d --- /dev/null +++ b/src/udev/udevadm-trigger.c @@ -0,0 +1,569 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "device-enumerator-private.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "id128-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "set.h" +#include "static-destruct.h" +#include "string-util.h" +#include "strv.h" +#include "udevadm.h" +#include "udevadm-util.h" +#include "udev-ctrl.h" +#include "virt.h" + +static bool arg_verbose = false; +static bool arg_dry_run = false; +static bool arg_quiet = false; +static bool arg_uuid = false; +static bool arg_settle = false; + +static int exec_list( + sd_device_enumerator *e, + sd_device_action_t action, + Set **ret_settle_path_or_ids) { + + _cleanup_set_free_ Set *settle_path_or_ids = NULL; + int uuid_supported = -1; + const char *action_str; + sd_device *d; + int r, ret = 0; + + assert(e); + + action_str = device_action_to_string(action); + + FOREACH_DEVICE_AND_SUBSYSTEM(e, d) { + sd_id128_t id = SD_ID128_NULL; + const char *syspath; + + r = sd_device_get_syspath(d, &syspath); + if (r < 0) { + log_debug_errno(r, "Failed to get syspath of enumerated devices, ignoring: %m"); + continue; + } + + if (arg_verbose) + printf("%s\n", syspath); + + if (arg_dry_run) + continue; + + /* Use the UUID mode if the user explicitly asked for it, or if --settle has been specified, + * so that we can recognize our own uevent. */ + r = sd_device_trigger_with_uuid(d, action, (arg_uuid || arg_settle) && uuid_supported != 0 ? &id : NULL); + if (r == -EINVAL && !arg_uuid && arg_settle && uuid_supported < 0) { + /* If we specified a UUID because of the settling logic, and we got EINVAL this might + * be caused by an old kernel which doesn't know the UUID logic (pre-4.13). Let's try + * if it works without the UUID logic then. */ + r = sd_device_trigger(d, action); + if (r != -EINVAL) + uuid_supported = false; /* dropping the uuid stuff changed the return code, + * hence don't bother next time */ + } + if (r < 0) { + /* ENOENT may be returned when a device does not have /uevent or is already + * removed. Hence, this is logged at debug level and ignored. + * + * ENODEV may be returned by some buggy device drivers e.g. /sys/devices/vio. + * See, + * https://github.com/systemd/systemd/issues/13652#issuecomment-535129791 and + * https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1845319. + * So, this error is ignored, but logged at warning level to encourage people to + * fix the driver. + * + * EROFS is returned when /sys is read only. In that case, all subsequent + * writes will also fail, hence return immediately. + * + * EACCES or EPERM may be returned when this is invoked by non-priviledged user. + * We do NOT return immediately, but continue operation and propagate the error. + * Why? Some device can be owned by a user, e.g., network devices configured in + * a network namespace. See, https://github.com/systemd/systemd/pull/18559 and + * https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ebb4a4bf76f164457184a3f43ebc1552416bc823 + * + * All other errors are logged at error level, but let's continue the operation, + * and propagate the error. + */ + + bool ignore = IN_SET(r, -ENOENT, -ENODEV); + int level = + arg_quiet ? LOG_DEBUG : + r == -ENOENT ? LOG_DEBUG : + r == -ENODEV ? LOG_WARNING : LOG_ERR; + + log_device_full_errno(d, level, r, + "Failed to write '%s' to '%s/uevent'%s: %m", + action_str, syspath, ignore ? ", ignoring" : ""); + + if (r == -EROFS) + return r; + if (ret == 0 && !ignore) + ret = r; + continue; + } else + log_device_debug(d, "Triggered device with action '%s'.", action_str); + + if (uuid_supported < 0) + uuid_supported = true; + + /* If the user asked for it, write event UUID to stdout */ + if (arg_uuid) + printf(SD_ID128_UUID_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id)); + + if (arg_settle) { + if (uuid_supported) { + sd_id128_t *dup; + + dup = newdup(sd_id128_t, &id, 1); + if (!dup) + return log_oom(); + + r = set_ensure_consume(&settle_path_or_ids, &id128_hash_ops_free, dup); + } else { + char *dup; + + dup = strdup(syspath); + if (!dup) + return log_oom(); + + r = set_ensure_consume(&settle_path_or_ids, &path_hash_ops_free, dup); + } + if (r < 0) + return log_oom(); + } + } + + if (ret_settle_path_or_ids) + *ret_settle_path_or_ids = TAKE_PTR(settle_path_or_ids); + + return ret; +} + +static int device_monitor_handler(sd_device_monitor *m, sd_device *dev, void *userdata) { + Set *settle_path_or_ids = * (Set**) ASSERT_PTR(userdata); + const char *syspath; + sd_id128_t id; + int r; + + assert(dev); + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to get syspath of device event, ignoring: %m"); + return 0; + } + + if (sd_device_get_trigger_uuid(dev, &id) >= 0) { + _cleanup_free_ sd_id128_t *saved = NULL; + + saved = set_remove(settle_path_or_ids, &id); + if (!saved) { + log_device_debug(dev, "Got uevent not matching expected UUID, ignoring."); + return 0; + } + } else { + _cleanup_free_ char *saved = NULL; + + saved = set_remove(settle_path_or_ids, syspath); + if (!saved) { + const char *old_sysname; + + /* When the device is renamed, the new name is broadcast, and the old name is saved + * in INTERFACE_OLD. + * + * TODO: remove support for INTERFACE_OLD when kernel baseline is bumped to 4.13 or + * higher. See 1193448cb68e5a90cab027e16a093bbd367e9494. + */ + + if (sd_device_get_property_value(dev, "INTERFACE_OLD", &old_sysname) >= 0) { + _cleanup_free_ char *dir = NULL, *old_syspath = NULL; + + r = path_extract_directory(syspath, &dir); + if (r < 0) { + log_device_debug_errno(dev, r, + "Failed to extract directory from '%s', ignoring: %m", + syspath); + return 0; + } + + old_syspath = path_join(dir, old_sysname); + if (!old_syspath) { + log_oom_debug(); + return 0; + } + + saved = set_remove(settle_path_or_ids, old_syspath); + } + } + if (!saved) { + log_device_debug(dev, "Got uevent for unexpected device, ignoring."); + return 0; + } + } + + if (arg_verbose) + printf("settle %s\n", syspath); + + if (arg_uuid) + printf("settle " SD_ID128_UUID_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id)); + + if (set_isempty(settle_path_or_ids)) + return sd_event_exit(sd_device_monitor_get_event(m), 0); + + return 0; +} + +static char* keyval(const char *str, const char **key, const char **val) { + char *buf, *pos; + + buf = strdup(str); + if (!buf) + return NULL; + + pos = strchr(buf, '='); + if (pos) { + pos[0] = 0; + pos++; + } + + *key = buf; + *val = pos; + + return buf; +} + +static int help(void) { + printf("%s trigger [OPTIONS] DEVPATH\n\n" + "Request events from the kernel.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -v --verbose Print the list of devices while running\n" + " -n --dry-run Do not actually trigger the events\n" + " -q --quiet Suppress error logging in triggering events\n" + " -t --type= Type of events to trigger\n" + " devices sysfs devices (default)\n" + " subsystems sysfs subsystems and drivers\n" + " all sysfs devices, subsystems, and drivers\n" + " -c --action=ACTION|help Event action value, default is \"change\"\n" + " -s --subsystem-match=SUBSYSTEM Trigger devices from a matching subsystem\n" + " -S --subsystem-nomatch=SUBSYSTEM Exclude devices from a matching subsystem\n" + " -a --attr-match=FILE[=VALUE] Trigger devices with a matching attribute\n" + " -A --attr-nomatch=FILE[=VALUE] Exclude devices with a matching attribute\n" + " -p --property-match=KEY=VALUE Trigger devices with a matching property\n" + " -g --tag-match=TAG Trigger devices with a matching tag\n" + " -y --sysname-match=NAME Trigger devices with this /sys path\n" + " --name-match=NAME Trigger devices with this /dev name\n" + " -b --parent-match=NAME Trigger devices with that parent device\n" + " --initialized-match Trigger devices that are already initialized\n" + " --initialized-nomatch Trigger devices that are not initialized yet\n" + " -w --settle Wait for the triggered events to complete\n" + " --wait-daemon[=SECONDS] Wait for udevd daemon to be initialized\n" + " before triggering uevents\n" + " --uuid Print synthetic uevent UUID\n" + " --prioritized-subsystem=SUBSYSTEM[,SUBSYSTEM…]\n" + " Trigger devices from a matching subsystem first\n", + program_invocation_short_name); + + return 0; +} + +int trigger_main(int argc, char *argv[], void *userdata) { + enum { + ARG_NAME = 0x100, + ARG_PING, + ARG_UUID, + ARG_PRIORITIZED_SUBSYSTEM, + ARG_INITIALIZED_MATCH, + ARG_INITIALIZED_NOMATCH, + }; + + static const struct option options[] = { + { "verbose", no_argument, NULL, 'v' }, + { "dry-run", no_argument, NULL, 'n' }, + { "quiet", no_argument, NULL, 'q' }, + { "type", required_argument, NULL, 't' }, + { "action", required_argument, NULL, 'c' }, + { "subsystem-match", required_argument, NULL, 's' }, + { "subsystem-nomatch", required_argument, NULL, 'S' }, + { "attr-match", required_argument, NULL, 'a' }, + { "attr-nomatch", required_argument, NULL, 'A' }, + { "property-match", required_argument, NULL, 'p' }, + { "tag-match", required_argument, NULL, 'g' }, + { "sysname-match", required_argument, NULL, 'y' }, + { "name-match", required_argument, NULL, ARG_NAME }, + { "parent-match", required_argument, NULL, 'b' }, + { "initialized-match", no_argument, NULL, ARG_INITIALIZED_MATCH }, + { "initialized-nomatch", no_argument, NULL, ARG_INITIALIZED_NOMATCH }, + { "settle", no_argument, NULL, 'w' }, + { "wait-daemon", optional_argument, NULL, ARG_PING }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + { "uuid", no_argument, NULL, ARG_UUID }, + { "prioritized-subsystem", required_argument, NULL, ARG_PRIORITIZED_SUBSYSTEM }, + {} + }; + enum { + TYPE_DEVICES, + TYPE_SUBSYSTEMS, + TYPE_ALL, + } device_type = TYPE_DEVICES; + sd_device_action_t action = SD_DEVICE_CHANGE; + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *m = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_set_free_ Set *settle_path_or_ids = NULL; + usec_t ping_timeout_usec = 5 * USEC_PER_SEC; + bool ping = false; + int c, r; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return r; + + while ((c = getopt_long(argc, argv, "vnqt:c:s:S:a:A:p:g:y:b:wVh", options, NULL)) >= 0) { + _cleanup_free_ char *buf = NULL; + const char *key, *val; + + switch (c) { + case 'v': + arg_verbose = true; + break; + case 'n': + arg_dry_run = true; + break; + case 'q': + arg_quiet = true; + break; + case 't': + if (streq(optarg, "devices")) + device_type = TYPE_DEVICES; + else if (streq(optarg, "subsystems")) + device_type = TYPE_SUBSYSTEMS; + else if (streq(optarg, "all")) + device_type = TYPE_ALL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type --type=%s", optarg); + break; + case 'c': + r = parse_device_action(optarg, &action); + if (r < 0) + return log_error_errno(r, "Unknown action '%s'", optarg); + if (r == 0) + return 0; + break; + case 's': + r = sd_device_enumerator_add_match_subsystem(e, optarg, true); + if (r < 0) + return log_error_errno(r, "Failed to add subsystem match '%s': %m", optarg); + break; + case 'S': + r = sd_device_enumerator_add_match_subsystem(e, optarg, false); + if (r < 0) + return log_error_errno(r, "Failed to add negative subsystem match '%s': %m", optarg); + break; + case 'a': + buf = keyval(optarg, &key, &val); + if (!buf) + return log_oom(); + r = sd_device_enumerator_add_match_sysattr(e, key, val, true); + if (r < 0) + return log_error_errno(r, "Failed to add sysattr match '%s=%s': %m", key, val); + break; + case 'A': + buf = keyval(optarg, &key, &val); + if (!buf) + return log_oom(); + r = sd_device_enumerator_add_match_sysattr(e, key, val, false); + if (r < 0) + return log_error_errno(r, "Failed to add negative sysattr match '%s=%s': %m", key, val); + break; + case 'p': + buf = keyval(optarg, &key, &val); + if (!buf) + return log_oom(); + r = sd_device_enumerator_add_match_property(e, key, val); + if (r < 0) + return log_error_errno(r, "Failed to add property match '%s=%s': %m", key, val); + break; + case 'g': + r = sd_device_enumerator_add_match_tag(e, optarg); + if (r < 0) + return log_error_errno(r, "Failed to add tag match '%s': %m", optarg); + break; + case 'y': + r = sd_device_enumerator_add_match_sysname(e, optarg); + if (r < 0) + return log_error_errno(r, "Failed to add sysname match '%s': %m", optarg); + break; + case 'b': { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(optarg, "/sys", &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", optarg); + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", optarg); + break; + } + case 'w': + arg_settle = true; + break; + + case ARG_NAME: { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(optarg, "/dev", &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", optarg); + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", optarg); + break; + } + + case ARG_PING: + ping = true; + if (optarg) { + r = parse_sec(optarg, &ping_timeout_usec); + if (r < 0) + log_error_errno(r, "Failed to parse timeout value '%s', ignoring: %m", optarg); + } + break; + + case ARG_UUID: + arg_uuid = true; + break; + + case ARG_PRIORITIZED_SUBSYSTEM: { + _cleanup_strv_free_ char **subsystems = NULL; + + subsystems = strv_split(optarg, ","); + if (!subsystems) + return log_error_errno(r, "Failed to parse prioritized subsystem '%s': %m", optarg); + + STRV_FOREACH(p, subsystems) { + r = device_enumerator_add_prioritized_subsystem(e, *p); + if (r < 0) + return log_error_errno(r, "Failed to add prioritized subsystem '%s': %m", *p); + } + break; + } + case ARG_INITIALIZED_MATCH: + case ARG_INITIALIZED_NOMATCH: + r = device_enumerator_add_match_is_initialized(e, c == ARG_INITIALIZED_MATCH ? MATCH_INITIALIZED_YES : MATCH_INITIALIZED_NO); + if (r < 0) + return log_error_errno(r, "Failed to set initialized filter: %m"); + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + } + + if (ping) { + _cleanup_(udev_ctrl_unrefp) UdevCtrl *uctrl = NULL; + + r = udev_ctrl_new(&uctrl); + if (r < 0) + return log_error_errno(r, "Failed to initialize udev control: %m"); + + r = udev_ctrl_send_ping(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to connect to udev daemon: %m"); + + r = udev_ctrl_wait(uctrl, ping_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to wait for daemon to reply: %m"); + } + + for (; optind < argc; optind++) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(argv[optind], NULL, &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", argv[optind]); + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", argv[optind]); + } + + if (arg_settle) { + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get default event: %m"); + + r = sd_device_monitor_new(&m); + if (r < 0) + return log_error_errno(r, "Failed to create device monitor object: %m"); + + r = sd_device_monitor_attach_event(m, event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to device monitor: %m"); + + r = sd_device_monitor_start(m, device_monitor_handler, &settle_path_or_ids); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + } + + switch (device_type) { + case TYPE_SUBSYSTEMS: + r = device_enumerator_scan_subsystems(e); + if (r < 0) + return log_error_errno(r, "Failed to scan subsystems: %m"); + break; + case TYPE_DEVICES: + r = device_enumerator_scan_devices(e); + if (r < 0) + return log_error_errno(r, "Failed to scan devices: %m"); + break; + case TYPE_ALL: + r = device_enumerator_scan_devices_and_subsystems(e); + if (r < 0) + return log_error_errno(r, "Failed to scan devices and subsystems: %m"); + break; + default: + assert_not_reached(); + } + + r = exec_list(e, action, arg_settle ? &settle_path_or_ids : NULL); + if (r < 0) + return r; + + if (!set_isempty(settle_path_or_ids)) { + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + } + + return 0; +} diff --git a/src/udev/udevadm-util.c b/src/udev/udevadm-util.c new file mode 100644 index 0000000..2447eda --- /dev/null +++ b/src/udev/udevadm-util.c @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> + +#include "alloc-util.h" +#include "bus-error.h" +#include "bus-util.h" +#include "device-private.h" +#include "path-util.h" +#include "udevadm-util.h" +#include "unit-name.h" + +static int find_device_from_unit(const char *unit_name, sd_device **ret) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *unit_path = NULL, *syspath = NULL; + int r; + + if (!unit_name_is_valid(unit_name, UNIT_NAME_PLAIN)) + return -EINVAL; + + if (unit_name_to_type(unit_name) != UNIT_DEVICE) + return -EINVAL; + + r = bus_connect_system_systemd(&bus); + if (r < 0) { + _cleanup_free_ char *path = NULL; + + log_debug_errno(r, "Failed to open connection to systemd, using unit name as syspath: %m"); + + r = unit_name_to_path(unit_name, &path); + if (r < 0) + return log_debug_errno(r, "Failed to convert \"%s\" to a device path: %m", unit_name); + + return sd_device_new_from_path(ret, path); + } + + unit_path = unit_dbus_path_from_name(unit_name); + if (!unit_path) + return -ENOMEM; + + r = sd_bus_get_property_string( + bus, + "org.freedesktop.systemd1", + unit_path, + "org.freedesktop.systemd1.Device", + "SysFSPath", + &error, + &syspath); + if (r < 0) + return log_debug_errno(r, "Failed to get SysFSPath= dbus property for %s: %s", + unit_name, bus_error_message(&error, r)); + + return sd_device_new_from_syspath(ret, syspath); +} + +int find_device(const char *id, const char *prefix, sd_device **ret) { + assert(id); + assert(ret); + + if (sd_device_new_from_path(ret, id) >= 0) + return 0; + + if (prefix && !path_startswith(id, prefix)) { + _cleanup_free_ char *path = NULL; + + path = path_join(prefix, id); + if (!path) + return -ENOMEM; + + if (sd_device_new_from_path(ret, path) >= 0) + return 0; + } + + /* if a path is provided, then it cannot be a unit name. Let's return earlier. */ + if (is_path(id)) + return -ENODEV; + + /* Check if the argument looks like a device unit name. */ + return find_device_from_unit(id, ret); +} + +int find_device_with_action(const char *id, sd_device_action_t action, sd_device **ret) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + int r; + + assert(id); + assert(ret); + assert(action >= 0 && action < _SD_DEVICE_ACTION_MAX); + + r = find_device(id, "/sys", &dev); + if (r < 0) + return r; + + r = device_read_uevent_file(dev); + if (r < 0) + return r; + + r = device_set_action(dev, action); + if (r < 0) + return r; + + *ret = TAKE_PTR(dev); + return 0; +} + +int parse_device_action(const char *str, sd_device_action_t *action) { + sd_device_action_t a; + + assert(str); + assert(action); + + if (streq(str, "help")) { + dump_device_action_table(); + return 0; + } + + a = device_action_from_string(str); + if (a < 0) + return a; + + *action = a; + return 1; +} diff --git a/src/udev/udevadm-util.h b/src/udev/udevadm-util.h new file mode 100644 index 0000000..7fb4556 --- /dev/null +++ b/src/udev/udevadm-util.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "sd-device.h" + +int find_device(const char *id, const char *prefix, sd_device **ret); +int find_device_with_action(const char *id, sd_device_action_t action, sd_device **ret); +int parse_device_action(const char *str, sd_device_action_t *action); diff --git a/src/udev/udevadm-verify.c b/src/udev/udevadm-verify.c new file mode 100644 index 0000000..3220250 --- /dev/null +++ b/src/udev/udevadm-verify.c @@ -0,0 +1,236 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "conf-files.h" +#include "constants.h" +#include "log.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "stat-util.h" +#include "static-destruct.h" +#include "strv.h" +#include "udev-rules.h" +#include "udevadm.h" + +static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY; +static char *arg_root = NULL; +static bool arg_summary = true; +static bool arg_style = true; + +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("udevadm", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s verify [OPTIONS] [FILE...]\n" + "\n%sVerify udev rules files.%s\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -N --resolve-names=early|never When to resolve names\n" + " --root=PATH Operate on an alternate filesystem root\n" + " --no-summary Do not show summary\n" + " --no-style Ignore style issues\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_ROOT = 0x100, + ARG_NO_SUMMARY, + ARG_NO_STYLE, + }; + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "resolve-names", required_argument, NULL, 'N' }, + { "root", required_argument, NULL, ARG_ROOT }, + { "no-summary", no_argument, NULL, ARG_NO_SUMMARY }, + { "no-style", no_argument, NULL, ARG_NO_STYLE }, + {} + }; + + int r, c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hVN:", options, NULL)) >= 0) + switch (c) { + case 'h': + return help(); + case 'V': + return print_version(); + case 'N': + arg_resolve_name_timing = resolve_name_timing_from_string(optarg); + if (arg_resolve_name_timing < 0) + return log_error_errno(arg_resolve_name_timing, + "--resolve-names= takes \"early\" or \"never\""); + /* + * In the verifier "late" has the effect of "never", + * and "never" would generate irrelevant diagnostics, + * so map "never" to "late". + */ + if (arg_resolve_name_timing == RESOLVE_NAME_NEVER) + arg_resolve_name_timing = RESOLVE_NAME_LATE; + break; + case ARG_ROOT: + r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root); + if (r < 0) + return r; + break; + case ARG_NO_SUMMARY: + arg_summary = false; + break; + + case ARG_NO_STYLE: + arg_style = false; + break; + + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (arg_root && optind < argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Combination of --root= and FILEs is not supported."); + + return 1; +} + +static int verify_rules_file(UdevRules *rules, const char *fname) { + UdevRuleFile *file; + int r; + + r = udev_rules_parse_file(rules, fname, /* extra_checks = */ true, &file); + if (r < 0) + return log_error_errno(r, "Failed to parse rules file %s: %m", fname); + if (r == 0) /* empty file. */ + return 0; + + unsigned issues = udev_rule_file_get_issues(file); + unsigned mask = (1U << LOG_ERR) | (1U << LOG_WARNING); + if (issues & mask) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: udev rules check failed.", fname); + + if (arg_style && (issues & (1U << LOG_NOTICE))) + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: udev rules have style issues.", fname); + + return 0; +} + +static int verify_rules_filelist(UdevRules *rules, char **files, size_t *fail_count, size_t *success_count, bool walk_dirs); + +static int verify_rules_dir(UdevRules *rules, const char *dir, size_t *fail_count, size_t *success_count) { + int r; + _cleanup_strv_free_ char **files = NULL; + + assert(rules); + assert(dir); + assert(fail_count); + assert(success_count); + + r = conf_files_list(&files, ".rules", NULL, 0, dir); + if (r < 0) + return log_error_errno(r, "Failed to enumerate rules files: %m"); + + return verify_rules_filelist(rules, files, fail_count, success_count, /* walk_dirs */ false); +} + +static int verify_rules_filelist(UdevRules *rules, char **files, size_t *fail_count, size_t *success_count, bool walk_dirs) { + int r, rv = 0; + + assert(rules); + assert(files); + assert(fail_count); + assert(success_count); + + STRV_FOREACH(fp, files) { + if (walk_dirs && is_dir(*fp, /* follow = */ true) > 0) + r = verify_rules_dir(rules, *fp, fail_count, success_count); + else { + r = verify_rules_file(rules, *fp); + if (r < 0) + ++(*fail_count); + else + ++(*success_count); + } + if (r < 0 && rv >= 0) + rv = r; + } + + return rv; +} + +static int verify_rules(UdevRules *rules, char **files) { + size_t fail_count = 0, success_count = 0; + int r; + + assert(rules); + assert(files); + + r = verify_rules_filelist(rules, files, &fail_count, &success_count, /* walk_dirs */ true); + + if (arg_summary) + printf("\n%s%zu udev rules files have been checked.%s\n" + " Success: %zu\n" + "%s Fail: %zu%s\n", + ansi_highlight(), + fail_count + success_count, + ansi_normal(), + success_count, + fail_count > 0 ? ansi_highlight_red() : "", + fail_count, + fail_count > 0 ? ansi_normal() : ""); + + return r; +} + +int verify_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + rules = udev_rules_new(arg_resolve_name_timing); + if (!rules) + return -ENOMEM; + + if (optind == argc) { + const char* const* rules_dirs = STRV_MAKE_CONST(CONF_PATHS("udev/rules.d")); + _cleanup_strv_free_ char **files = NULL; + + r = conf_files_list_strv(&files, ".rules", arg_root, 0, rules_dirs); + if (r < 0) + return log_error_errno(r, "Failed to enumerate rules files: %m"); + if (arg_root && strv_isempty(files)) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "No rules files found in %s.", arg_root); + + return verify_rules(rules, files); + } + + return verify_rules(rules, strv_skip(argv, optind)); +} diff --git a/src/udev/udevadm-wait.c b/src/udev/udevadm-wait.c new file mode 100644 index 0000000..e6620c2 --- /dev/null +++ b/src/udev/udevadm-wait.c @@ -0,0 +1,456 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <getopt.h> +#include <unistd.h> + +#include "sd-event.h" + +#include "alloc-util.h" +#include "chase.h" +#include "device-monitor-private.h" +#include "device-util.h" +#include "errno-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "inotify-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "static-destruct.h" +#include "string-table.h" +#include "strv.h" +#include "udev-util.h" +#include "udevadm.h" + +typedef enum WaitUntil { + WAIT_UNTIL_INITIALIZED, + WAIT_UNTIL_ADDED, + WAIT_UNTIL_REMOVED, + _WAIT_UNTIL_MAX, + _WAIT_UNTIL_INVALID = -EINVAL, +} WaitUntil; + +static WaitUntil arg_wait_until = WAIT_UNTIL_INITIALIZED; +static usec_t arg_timeout_usec = USEC_INFINITY; +static bool arg_settle = false; +static char **arg_devices = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_devices, strv_freep); + +static const char * const wait_until_table[_WAIT_UNTIL_MAX] = { + [WAIT_UNTIL_INITIALIZED] = "initialized", + [WAIT_UNTIL_ADDED] = "added", + [WAIT_UNTIL_REMOVED] = "removed", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(wait_until, WaitUntil); + +static int check_device(const char *path) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + int r; + + assert(path); + + if (arg_wait_until == WAIT_UNTIL_REMOVED) { + r = laccess(path, F_OK); + if (r == -ENOENT) + return true; + if (r < 0) + return r; + return false; + } + + r = sd_device_new_from_path(&dev, path); + if (r == -ENODEV) + return false; + if (r < 0) + return r; + + if (arg_wait_until == WAIT_UNTIL_INITIALIZED) + return sd_device_get_is_initialized(dev); + + return true; +} + +static bool check(void) { + int r; + + if (arg_settle) { + r = udev_queue_is_empty(); + if (r == 0) + return false; + if (r < 0) + log_warning_errno(r, "Failed to check if udev queue is empty, assuming empty: %m"); + } + + STRV_FOREACH(p, arg_devices) { + r = check_device(*p); + if (r <= 0) { + if (r < 0) + log_warning_errno(r, "Failed to check if device \"%s\" is %s, assuming not %s: %m", + *p, + wait_until_to_string(arg_wait_until), + wait_until_to_string(arg_wait_until)); + return false; + } + } + + return true; +} + +static int check_and_exit(sd_event *event) { + int r; + + assert(event); + + if (check()) { + r = sd_event_exit(event, 0); + if (r < 0) + return r; + + return 1; + } + + return 0; +} + +static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) { + const char *name; + int r; + + assert(monitor); + assert(device); + + if (device_for_action(device, SD_DEVICE_REMOVE) != (arg_wait_until == WAIT_UNTIL_REMOVED)) + return 0; + + if (arg_wait_until == WAIT_UNTIL_REMOVED) + /* On removed event, the received device may not contain enough information. + * Let's unconditionally check all requested devices are removed. */ + return check_and_exit(sd_device_monitor_get_event(monitor)); + + /* For other events, at first check if the received device matches with the requested devices, + * to avoid calling check() so many times within a short time. */ + + r = sd_device_get_sysname(device, &name); + if (r < 0) { + log_device_warning_errno(device, r, "Failed to get sysname of received device, ignoring: %m"); + return 0; + } + + STRV_FOREACH(p, arg_devices) { + const char *s; + + if (!path_startswith(*p, "/sys")) + continue; + + r = path_find_last_component(*p, false, NULL, &s); + if (r < 0) { + log_warning_errno(r, "Failed to extract filename from \"%s\", ignoring: %m", *p); + continue; + } + if (r == 0) + continue; + + if (strneq(s, name, r)) + return check_and_exit(sd_device_monitor_get_event(monitor)); + } + + r = sd_device_get_devname(device, &name); + if (r < 0) { + if (r != -ENOENT) + log_device_warning_errno(device, r, "Failed to get devname of received device, ignoring: %m"); + return 0; + } + + if (path_strv_contains(arg_devices, name)) + return check_and_exit(sd_device_monitor_get_event(monitor)); + + FOREACH_DEVICE_DEVLINK(device, link) + if (path_strv_contains(arg_devices, link)) + return check_and_exit(sd_device_monitor_get_event(monitor)); + + return 0; +} + +static int setup_monitor(sd_event *event, MonitorNetlinkGroup group, const char *description, sd_device_monitor **ret) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + int r; + + assert(event); + assert(ret); + + r = device_monitor_new_full(&monitor, group, /* fd = */ -1); + if (r < 0) + return r; + + r = sd_device_monitor_attach_event(monitor, event); + if (r < 0) + return r; + + r = sd_device_monitor_set_description(monitor, description); + if (r < 0) + return r; + + r = sd_device_monitor_start(monitor, device_monitor_handler, NULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(monitor); + return 0; +} + +static int on_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) { + return check_and_exit(sd_event_source_get_event(s)); +} + +static int setup_inotify(sd_event *event) { + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + int r; + + assert(event); + + if (!arg_settle) + return 0; + + r = sd_event_add_inotify(event, &s, "/run/udev" , IN_CREATE | IN_DELETE, on_inotify, NULL); + if (r < 0) + return r; + + r = sd_event_source_set_description(s, "inotify-event-source"); + if (r < 0) + return r; + + return sd_event_source_set_floating(s, true); +} + +static int setup_timer(sd_event *event) { + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + int r; + + assert(event); + + if (arg_timeout_usec == USEC_INFINITY) + return 0; + + r = sd_event_add_time_relative(event, &s, CLOCK_BOOTTIME, arg_timeout_usec, 0, + NULL, INT_TO_PTR(-ETIMEDOUT)); + if (r < 0) + return r; + + r = sd_event_source_set_description(s, "timeout-event-source"); + if (r < 0) + return r; + + return sd_event_source_set_floating(s, true); +} + +static int reset_timer(sd_event *e, sd_event_source **s); + +static int on_periodic_timer(sd_event_source *s, uint64_t usec, void *userdata) { + static unsigned counter = 0; + sd_event *e; + int r; + + assert(s); + + e = sd_event_source_get_event(s); + + /* Even if all devices exists, we try to wait for uevents to be emitted from kernel. */ + if (check()) + counter++; + else + counter = 0; + + if (counter >= 2) { + log_debug("All requested devices popped up without receiving kernel uevents."); + return sd_event_exit(e, 0); + } + + r = reset_timer(e, &s); + if (r < 0) + log_warning_errno(r, "Failed to reset periodic timer event source, ignoring: %m"); + + return 0; +} + +static int reset_timer(sd_event *e, sd_event_source **s) { + return event_reset_time_relative(e, s, CLOCK_BOOTTIME, 250 * USEC_PER_MSEC, 0, + on_periodic_timer, NULL, 0, "periodic-timer-event-source", false); +} + +static int setup_periodic_timer(sd_event *event) { + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + int r; + + assert(event); + + r = reset_timer(event, &s); + if (r < 0) + return r; + + /* Set the lower priority than device monitor, to make uevents always dispatched first. */ + r = sd_event_source_set_priority(s, SD_EVENT_PRIORITY_NORMAL + 1); + if (r < 0) + return r; + + return sd_event_source_set_floating(s, true); +} + +static int help(void) { + printf("%s wait [OPTIONS] DEVICE [DEVICE…]\n\n" + "Wait for devices or device symlinks being created.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -t --timeout=SEC Maximum time to wait for the device\n" + " --initialized=BOOL Wait for devices being initialized by systemd-udevd\n" + " --removed Wait for devices being removed\n" + " --settle Also wait for all queued events being processed\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_INITIALIZED = 0x100, + ARG_REMOVED, + ARG_SETTLE, + }; + + static const struct option options[] = { + { "timeout", required_argument, NULL, 't' }, + { "initialized", required_argument, NULL, ARG_INITIALIZED }, + { "removed", no_argument, NULL, ARG_REMOVED }, + { "settle", no_argument, NULL, ARG_SETTLE }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + {} + }; + + int c, r; + + while ((c = getopt_long(argc, argv, "t:hV", options, NULL)) >= 0) + switch (c) { + case 't': + r = parse_sec(optarg, &arg_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to parse -t/--timeout= parameter: %s", optarg); + break; + + case ARG_INITIALIZED: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --initialized= parameter: %s", optarg); + arg_wait_until = r ? WAIT_UNTIL_INITIALIZED : WAIT_UNTIL_ADDED; + break; + + case ARG_REMOVED: + arg_wait_until = WAIT_UNTIL_REMOVED; + break; + + case ARG_SETTLE: + arg_settle = true; + break; + + case 'V': + return print_version(); + + case 'h': + return help(); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (optind >= argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Too few arguments, expected at least one device path or device symlink."); + + arg_devices = strv_copy(argv + optind); + if (!arg_devices) + return log_oom(); + + return 1; /* work to do */ +} + +int wait_main(int argc, char *argv[], void *userdata) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *udev_monitor = NULL, *kernel_monitor = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + STRV_FOREACH(p, arg_devices) { + path_simplify(*p); + + if (!path_is_safe(*p)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Device path cannot contain \"..\"."); + + if (!is_device_path(*p)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Specified path \"%s\" does not start with \"/dev/\" or \"/sys/\".", *p); + } + + /* Check before configuring event sources, as devices may be already initialized. */ + if (check()) + return 0; + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to initialize sd-event: %m"); + + r = setup_timer(event); + if (r < 0) + return log_error_errno(r, "Failed to set up timeout: %m"); + + r = setup_inotify(event); + if (r < 0) + return log_error_errno(r, "Failed to set up inotify: %m"); + + r = setup_monitor(event, MONITOR_GROUP_UDEV, "udev-uevent-monitor-event-source", &udev_monitor); + if (r < 0) + return log_error_errno(r, "Failed to set up udev uevent monitor: %m"); + + if (arg_wait_until == WAIT_UNTIL_ADDED) { + /* If --initialized=no is specified, it is not necessary to wait uevents for the specified + * devices to be processed by udevd. Hence, let's listen on the kernel's uevent stream. Then, + * we may be able to finish this program earlier when udevd is very busy. + * Note, we still need to also setup udev monitor, as this may be invoked with a devlink + * (e.g. /dev/disk/by-id/foo). In that case, the devlink may not exist when we received a + * uevent from kernel, as the udevd may not finish to process the uevent yet. Hence, we need + * to wait until the event is processed by udevd. */ + r = setup_monitor(event, MONITOR_GROUP_KERNEL, "kernel-uevent-monitor-event-source", &kernel_monitor); + if (r < 0) + return log_error_errno(r, "Failed to set up kernel uevent monitor: %m"); + + /* This is a workaround for issues #24360 and #24450. + * For some reasons, the kernel sometimes does not emit uevents for loop block device on + * attach. Hence, without the periodic timer, no event source for this program will be + * triggered, and this will be timed out. + * Theoretically, inotify watch may be better, but this program typically expected to run in + * a short time. Hence, let's use the simpler periodic timer event source here. */ + r = setup_periodic_timer(event); + if (r < 0) + return log_error_errno(r, "Failed to set up periodic timer: %m"); + } + + /* Check before entering the event loop, as devices may be initialized during setting up event sources. */ + if (check()) + return 0; + + r = sd_event_loop(event); + if (r == -ETIMEDOUT) + return log_error_errno(r, "Timed out for waiting devices being %s.", + wait_until_to_string(arg_wait_until)); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} diff --git a/src/udev/udevadm.c b/src/udev/udevadm.c new file mode 100644 index 0000000..687b927 --- /dev/null +++ b/src/udev/udevadm.c @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> + +#include "alloc-util.h" +#include "main-func.h" +#include "pretty-print.h" +#include "process-util.h" +#include "selinux-util.h" +#include "string-util.h" +#include "udev-util.h" +#include "udevadm.h" +#include "udevd.h" +#include "verbs.h" + +static int help(void) { + static const char *const short_descriptions[][2] = { + { "info", "Query sysfs or the udev database" }, + { "trigger", "Request events from the kernel" }, + { "settle", "Wait for pending udev events" }, + { "control", "Control the udev daemon" }, + { "monitor", "Listen to kernel and udev events" }, + { "test", "Test an event run" }, + { "test-builtin", "Test a built-in command" }, + { "verify", "Verify udev rules files" }, + { "wait", "Wait for device or device symlink" }, + { "lock", "Lock a block device" }, + }; + + _cleanup_free_ char *link = NULL; + size_t i; + int r; + + r = terminal_urlify_man("udevadm", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s [--help] [--version] [--debug] COMMAND [COMMAND OPTIONS]\n\n" + "Send control commands or test the device manager.\n\n" + "Commands:\n", + program_invocation_short_name); + + for (i = 0; i < ELEMENTSOF(short_descriptions); i++) + printf(" %-12s %s\n", short_descriptions[i][0], short_descriptions[i][1]); + + printf("\nSee the %s for details.\n", link); + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + {} + }; + int c; + + assert(argc >= 0); + assert(argv); + + /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long() + * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */ + optind = 0; + while ((c = getopt_long(argc, argv, "+dhV", options, NULL)) >= 0) + switch (c) { + + case 'd': + log_set_max_level(LOG_DEBUG); + break; + + case 'h': + return help(); + + case 'V': + return print_version(); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; /* work to do */ +} + +static int version_main(int argc, char *argv[], void *userdata) { + return print_version(); +} + +static int help_main(int argc, char *argv[], void *userdata) { + return help(); +} + +static int udevadm_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "info", VERB_ANY, VERB_ANY, 0, info_main }, + { "trigger", VERB_ANY, VERB_ANY, 0, trigger_main }, + { "settle", VERB_ANY, VERB_ANY, 0, settle_main }, + { "control", VERB_ANY, VERB_ANY, 0, control_main }, + { "monitor", VERB_ANY, VERB_ANY, 0, monitor_main }, + { "hwdb", VERB_ANY, VERB_ANY, 0, hwdb_main }, + { "test", VERB_ANY, VERB_ANY, 0, test_main }, + { "test-builtin", VERB_ANY, VERB_ANY, 0, builtin_main }, + { "wait", VERB_ANY, VERB_ANY, 0, wait_main }, + { "lock", VERB_ANY, VERB_ANY, 0, lock_main }, + { "verify", VERB_ANY, VERB_ANY, 0, verify_main }, + { "version", VERB_ANY, VERB_ANY, 0, version_main }, + { "help", VERB_ANY, VERB_ANY, 0, help_main }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + if (invoked_as(argv, "udevd")) + return run_udevd(argc, argv); + + udev_parse_config(); + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = mac_init(); + if (r < 0) + return r; + + return udevadm_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/udev/udevadm.h b/src/udev/udevadm.h new file mode 100644 index 0000000..7920a70 --- /dev/null +++ b/src/udev/udevadm.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdio.h> + +#include "macro.h" + +int info_main(int argc, char *argv[], void *userdata); +int trigger_main(int argc, char *argv[], void *userdata); +int settle_main(int argc, char *argv[], void *userdata); +int control_main(int argc, char *argv[], void *userdata); +int monitor_main(int argc, char *argv[], void *userdata); +int hwdb_main(int argc, char *argv[], void *userdata); +int test_main(int argc, char *argv[], void *userdata); +int builtin_main(int argc, char *argv[], void *userdata); +int verify_main(int argc, char *argv[], void *userdata); +int wait_main(int argc, char *argv[], void *userdata); +int lock_main(int argc, char *argv[], void *userdata); + +static inline int print_version(void) { + /* Dracut relies on the version being a single integer */ + puts(STRINGIFY(PROJECT_VERSION)); + return 0; +} diff --git a/src/udev/udevd.c b/src/udev/udevd.c new file mode 100644 index 0000000..2ed4282 --- /dev/null +++ b/src/udev/udevd.c @@ -0,0 +1,408 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2004 Chris Friesen <chris_friesen@sympatico.ca> + * Copyright © 2009 Canonical Ltd. + * Copyright © 2009 Scott James Remnant <scott@netsplit.com> + */ + +#include <getopt.h> +#include <unistd.h> + +#include "sd-daemon.h" + +#include "env-file.h" +#include "errno-util.h" +#include "fd-util.h" +#include "mkdir.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "proc-cmdline.h" +#include "process-util.h" +#include "rlimit-util.h" +#include "selinux-util.h" +#include "signal-util.h" +#include "syslog-util.h" +#include "udev-manager.h" +#include "udev-util.h" +#include "udevd.h" +#include "version.h" + +static bool arg_debug = false; +static int arg_daemonize = false; + +static int listen_fds(int *ret_ctrl, int *ret_netlink) { + int ctrl_fd = -EBADF, netlink_fd = -EBADF; + int fd, n; + + assert(ret_ctrl); + assert(ret_netlink); + + n = sd_listen_fds(true); + if (n < 0) + return n; + + for (fd = SD_LISTEN_FDS_START; fd < n + SD_LISTEN_FDS_START; fd++) { + if (sd_is_socket(fd, AF_UNIX, SOCK_SEQPACKET, -1) > 0) { + if (ctrl_fd >= 0) + return -EINVAL; + ctrl_fd = fd; + continue; + } + + if (sd_is_socket(fd, AF_NETLINK, SOCK_RAW, -1) > 0) { + if (netlink_fd >= 0) + return -EINVAL; + netlink_fd = fd; + continue; + } + + return -EINVAL; + } + + *ret_ctrl = ctrl_fd; + *ret_netlink = netlink_fd; + + return 0; +} + +static int manager_parse_udev_config(Manager *manager) { + _cleanup_free_ char *log_val = NULL, *children_max = NULL, *exec_delay = NULL, + *event_timeout = NULL, *resolve_names = NULL, *timeout_signal = NULL; + int r; + + assert(manager); + + r = parse_env_file(NULL, "/etc/udev/udev.conf", + "udev_log", &log_val, + "children_max", &children_max, + "exec_delay", &exec_delay, + "event_timeout", &event_timeout, + "resolve_names", &resolve_names, + "timeout_signal", &timeout_signal); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + r = udev_set_max_log_level(log_val); + if (r < 0) + log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r, + "Failed to set udev log level '%s', ignoring: %m", log_val); + + if (children_max) { + r = safe_atou(children_max, &manager->children_max); + if (r < 0) + log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r, + "Failed to parse children_max=%s, ignoring: %m", children_max); + } + + if (exec_delay) { + r = parse_sec(exec_delay, &manager->exec_delay_usec); + if (r < 0) + log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r, + "Failed to parse exec_delay=%s, ignoring: %m", exec_delay); + } + + if (event_timeout) { + r = parse_sec(event_timeout, &manager->timeout_usec); + if (r < 0) + log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r, + "Failed to parse event_timeout=%s, ignoring: %m", event_timeout); + } + + if (resolve_names) { + ResolveNameTiming t; + + t = resolve_name_timing_from_string(resolve_names); + if (t < 0) + log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r, + "Failed to parse resolve_names=%s, ignoring.", resolve_names); + else + manager->resolve_name_timing = t; + } + + if (timeout_signal) { + r = signal_from_string(timeout_signal); + if (r < 0) + log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r, + "Failed to parse timeout_signal=%s, ignoring: %m", timeout_signal); + else + manager->timeout_signal = r; + } + + return 0; +} + +/* + * read the kernel command line, in case we need to get into debug mode + * udev.log_level=<level> syslog priority + * udev.children_max=<number of workers> events are fully serialized if set to 1 + * udev.exec_delay=<number of seconds> delay execution of every executed program + * udev.event_timeout=<number of seconds> seconds to wait before terminating an event + * udev.blockdev_read_only<=bool> mark all block devices read-only when they appear + */ +static int parse_proc_cmdline_item(const char *key, const char *value, void *data) { + Manager *manager = ASSERT_PTR(data); + int r; + + assert(key); + + if (proc_cmdline_key_streq(key, "udev.log_level") || + proc_cmdline_key_streq(key, "udev.log_priority")) { /* kept for backward compatibility */ + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = log_level_from_string(value); + if (r >= 0) + log_set_max_level(r); + + } else if (proc_cmdline_key_streq(key, "udev.event_timeout")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = parse_sec(value, &manager->timeout_usec); + + } else if (proc_cmdline_key_streq(key, "udev.children_max")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = safe_atou(value, &manager->children_max); + + } else if (proc_cmdline_key_streq(key, "udev.exec_delay")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = parse_sec(value, &manager->exec_delay_usec); + + } else if (proc_cmdline_key_streq(key, "udev.timeout_signal")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = signal_from_string(value); + if (r > 0) + manager->timeout_signal = r; + + } else if (proc_cmdline_key_streq(key, "udev.blockdev_read_only")) { + + if (!value) + manager->blockdev_read_only = true; + else { + r = parse_boolean(value); + if (r < 0) + log_warning_errno(r, "Failed to parse udev.blockdev-read-only argument, ignoring: %s", value); + else + manager->blockdev_read_only = r; + } + + if (manager->blockdev_read_only) + log_notice("All physical block devices will be marked read-only."); + + return 0; + + } else { + if (startswith(key, "udev.")) + log_warning("Unknown udev kernel command line option \"%s\", ignoring.", key); + + return 0; + } + + if (r < 0) + log_warning_errno(r, "Failed to parse \"%s=%s\", ignoring: %m", key, value); + + return 0; +} + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-udevd.service", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...]\n\n" + "Rule-based manager for device events and files.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -d --daemon Detach and run in the background\n" + " -D --debug Enable debug output\n" + " -c --children-max=INT Set maximum number of workers\n" + " -e --exec-delay=SECONDS Seconds to wait before executing RUN=\n" + " -t --event-timeout=SECONDS Seconds to wait before terminating an event\n" + " -N --resolve-names=early|late|never\n" + " When to resolve users and groups\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[], Manager *manager) { + enum { + ARG_TIMEOUT_SIGNAL, + }; + + static const struct option options[] = { + { "daemon", no_argument, NULL, 'd' }, + { "debug", no_argument, NULL, 'D' }, + { "children-max", required_argument, NULL, 'c' }, + { "exec-delay", required_argument, NULL, 'e' }, + { "event-timeout", required_argument, NULL, 't' }, + { "resolve-names", required_argument, NULL, 'N' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "timeout-signal", required_argument, NULL, ARG_TIMEOUT_SIGNAL }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + assert(manager); + + while ((c = getopt_long(argc, argv, "c:de:Dt:N:hV", options, NULL)) >= 0) { + switch (c) { + + case 'd': + arg_daemonize = true; + break; + case 'c': + r = safe_atou(optarg, &manager->children_max); + if (r < 0) + log_warning_errno(r, "Failed to parse --children-max= value '%s', ignoring: %m", optarg); + break; + case 'e': + r = parse_sec(optarg, &manager->exec_delay_usec); + if (r < 0) + log_warning_errno(r, "Failed to parse --exec-delay= value '%s', ignoring: %m", optarg); + break; + case ARG_TIMEOUT_SIGNAL: + r = signal_from_string(optarg); + if (r <= 0) + log_warning_errno(r, "Failed to parse --timeout-signal= value '%s', ignoring: %m", optarg); + else + manager->timeout_signal = r; + + break; + case 't': + r = parse_sec(optarg, &manager->timeout_usec); + if (r < 0) + log_warning_errno(r, "Failed to parse --event-timeout= value '%s', ignoring: %m", optarg); + break; + case 'D': + arg_debug = true; + break; + case 'N': { + ResolveNameTiming t; + + t = resolve_name_timing_from_string(optarg); + if (t < 0) + log_warning("Invalid --resolve-names= value '%s', ignoring.", optarg); + else + manager->resolve_name_timing = t; + break; + } + case 'h': + return help(); + case 'V': + printf("%s\n", GIT_VERSION); + return 0; + case '?': + return -EINVAL; + default: + assert_not_reached(); + + } + } + + return 1; +} + +int run_udevd(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *manager = NULL; + int fd_ctrl = -EBADF, fd_uevent = -EBADF; + int r; + + log_set_target(LOG_TARGET_AUTO); + log_open(); + + manager = manager_new(); + if (!manager) + return log_oom(); + + manager_parse_udev_config(manager); + + log_parse_environment(); + log_open(); /* Done again to update after reading configuration. */ + + r = parse_argv(argc, argv, manager); + if (r <= 0) + return r; + + r = proc_cmdline_parse(parse_proc_cmdline_item, manager, PROC_CMDLINE_STRIP_RD_PREFIX); + if (r < 0) + log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m"); + + if (arg_debug) { + log_set_target(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_DEBUG); + } + + r = must_be_root(); + if (r < 0) + return r; + + /* set umask before creating any file/directory */ + umask(022); + + r = mac_init(); + if (r < 0) + return r; + + /* Make sure we can have plenty fds (for example for pidfds) */ + (void) rlimit_nofile_bump(-1); + + r = RET_NERRNO(mkdir("/run/udev", 0755)); + if (r < 0 && r != -EEXIST) + return log_error_errno(r, "Failed to create /run/udev: %m"); + + r = listen_fds(&fd_ctrl, &fd_uevent); + if (r < 0) + return log_error_errno(r, "Failed to listen on fds: %m"); + + r = manager_init(manager, fd_ctrl, fd_uevent); + if (r < 0) + return log_error_errno(r, "Failed to create manager: %m"); + + if (arg_daemonize) { + pid_t pid; + + log_info("Starting systemd-udevd version " GIT_VERSION); + + /* connect /dev/null to stdin, stdout, stderr */ + if (log_get_max_level() < LOG_DEBUG) { + r = make_null_stdio(); + if (r < 0) + log_warning_errno(r, "Failed to redirect standard streams to /dev/null: %m"); + } + + pid = fork(); + if (pid < 0) + return log_error_errno(errno, "Failed to fork daemon: %m"); + if (pid > 0) + /* parent */ + return 0; + + /* child */ + (void) setsid(); + } + + return manager_main(manager); +} diff --git a/src/udev/udevd.h b/src/udev/udevd.h new file mode 100644 index 0000000..583e895 --- /dev/null +++ b/src/udev/udevd.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +int run_udevd(int argc, char *argv[]); diff --git a/src/udev/v4l_id/v4l_id.c b/src/udev/v4l_id/v4l_id.c new file mode 100644 index 0000000..30527e9 --- /dev/null +++ b/src/udev/v4l_id/v4l_id.c @@ -0,0 +1,113 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (c) 2009 Filippo Argiolas <filippo.argiolas@gmail.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details: + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/time.h> +#include <sys/types.h> +#include <unistd.h> +#include <linux/videodev2.h> + +#include "build.h" +#include "fd-util.h" +#include "main-func.h" + +static const char *arg_device = NULL; + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'v' }, + {} + }; + int c; + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + case 'h': + printf("%s [OPTIONS...] DEVICE\n\n" + "Video4Linux device identification.\n\n" + " -h --help Show this help text\n" + " --version Show package version\n", + program_invocation_short_name); + return 0; + case 'v': + return version(); + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (!argv[optind]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "DEVICE argument missing."); + + arg_device = argv[optind]; + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_close_ int fd = -EBADF; + struct v4l2_capability v2cap; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + fd = open(arg_device, O_RDONLY|O_CLOEXEC|O_NOCTTY); + if (fd < 0) + return log_error_errno(errno, "Failed to open %s: %m", arg_device); + + if (ioctl(fd, VIDIOC_QUERYCAP, &v2cap) == 0) { + int capabilities; + + printf("ID_V4L_VERSION=2\n"); + printf("ID_V4L_PRODUCT=%s\n", v2cap.card); + printf("ID_V4L_CAPABILITIES=:"); + + if (v2cap.capabilities & V4L2_CAP_DEVICE_CAPS) + capabilities = v2cap.device_caps; + else + capabilities = v2cap.capabilities; + + if ((capabilities & V4L2_CAP_VIDEO_CAPTURE) > 0 || + (capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) > 0) + printf("capture:"); + if ((capabilities & V4L2_CAP_VIDEO_OUTPUT) > 0 || + (capabilities & V4L2_CAP_VIDEO_OUTPUT_MPLANE) > 0) + printf("video_output:"); + if ((capabilities & V4L2_CAP_VIDEO_OVERLAY) > 0) + printf("video_overlay:"); + if ((capabilities & V4L2_CAP_AUDIO) > 0) + printf("audio:"); + if ((capabilities & V4L2_CAP_TUNER) > 0) + printf("tuner:"); + if ((capabilities & V4L2_CAP_RADIO) > 0) + printf("radio:"); + printf("\n"); + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); |