diff options
Diffstat (limited to 'src/udev')
64 files changed, 17886 insertions, 0 deletions
diff --git a/src/udev/.vimrc b/src/udev/.vimrc new file mode 100644 index 0000000..366fbdc --- /dev/null +++ b/src/udev/.vimrc @@ -0,0 +1,4 @@ +" 'set exrc' in ~/.vimrc will read .vimrc from the current directory +set tabstop=8 +set shiftwidth=8 +set expandtab diff --git a/src/udev/ata_id/ata_id.c b/src/udev/ata_id/ata_id.c new file mode 100644 index 0000000..ce0bf5d --- /dev/null +++ b/src/udev/ata_id/ata_id.c @@ -0,0 +1,651 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * ata_id - reads product/serial number from ATA drives + * + * Copyright © 2009-2010 David Zeuthen <zeuthen@gmail.com> + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <linux/bsg.h> +#include <linux/hdreg.h> +#include <scsi/scsi.h> +#include <scsi/scsi_ioctl.h> +#include <scsi/sg.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "fd-util.h" +#include "libudev-util.h" +#include "log.h" +#include "memory-util.h" +#include "udev-util.h" + +#define COMMAND_TIMEOUT_MSEC (30 * 1000) + +static int disk_scsi_inquiry_command( + int fd, + void *buf, + size_t buf_len) { + + uint8_t cdb[6] = { + /* INQUIRY, see SPC-4 section 6.4 */ + [0] = 0x12, /* OPERATION CODE: INQUIRY */ + [3] = (buf_len >> 8), /* ALLOCATION LENGTH */ + [4] = (buf_len & 0xff), + }; + uint8_t sense[32] = {}; + struct sg_io_v4 io_v4 = { + .guard = 'Q', + .protocol = BSG_PROTOCOL_SCSI, + .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD, + .request_len = sizeof(cdb), + .request = (uintptr_t) cdb, + .max_response_len = sizeof(sense), + .response = (uintptr_t) sense, + .din_xfer_len = buf_len, + .din_xferp = (uintptr_t) buf, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + int ret; + + ret = ioctl(fd, SG_IO, &io_v4); + if (ret != 0) { + /* could be that the driver doesn't do version 4, try version 3 */ + if (errno == EINVAL) { + struct sg_io_hdr io_hdr = { + .interface_id = 'S', + .cmdp = (unsigned char*) cdb, + .cmd_len = sizeof (cdb), + .dxferp = buf, + .dxfer_len = buf_len, + .sbp = sense, + .mx_sb_len = sizeof(sense), + .dxfer_direction = SG_DXFER_FROM_DEV, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + ret = ioctl(fd, SG_IO, &io_hdr); + if (ret != 0) + return ret; + + /* even if the ioctl succeeds, we need to check the return value */ + if (!(io_hdr.status == 0 && + io_hdr.host_status == 0 && + io_hdr.driver_status == 0)) { + errno = EIO; + return -1; + } + } else + return ret; + } + + /* even if the ioctl succeeds, we need to check the return value */ + if (!(io_v4.device_status == 0 && + io_v4.transport_status == 0 && + io_v4.driver_status == 0)) { + errno = EIO; + return -1; + } + + return 0; +} + +static int disk_identify_command( + int fd, + void *buf, + size_t buf_len) { + + uint8_t cdb[12] = { + /* + * ATA Pass-Through 12 byte command, as described in + * + * T10 04-262r8 ATA Command Pass-Through + * + * from http://www.t10.org/ftp/t10/document.04/04-262r8.pdf + */ + [0] = 0xa1, /* OPERATION CODE: 12 byte pass through */ + [1] = 4 << 1, /* PROTOCOL: PIO Data-in */ + [2] = 0x2e, /* OFF_LINE=0, CK_COND=1, T_DIR=1, BYT_BLOK=1, T_LENGTH=2 */ + [3] = 0, /* FEATURES */ + [4] = 1, /* SECTORS */ + [5] = 0, /* LBA LOW */ + [6] = 0, /* LBA MID */ + [7] = 0, /* LBA HIGH */ + [8] = 0 & 0x4F, /* SELECT */ + [9] = 0xEC, /* Command: ATA IDENTIFY DEVICE */ + }; + uint8_t sense[32] = {}; + uint8_t *desc = sense + 8; + struct sg_io_v4 io_v4 = { + .guard = 'Q', + .protocol = BSG_PROTOCOL_SCSI, + .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD, + .request_len = sizeof(cdb), + .request = (uintptr_t) cdb, + .max_response_len = sizeof(sense), + .response = (uintptr_t) sense, + .din_xfer_len = buf_len, + .din_xferp = (uintptr_t) buf, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + int ret; + + ret = ioctl(fd, SG_IO, &io_v4); + if (ret != 0) { + /* could be that the driver doesn't do version 4, try version 3 */ + if (errno == EINVAL) { + struct sg_io_hdr io_hdr = { + .interface_id = 'S', + .cmdp = (unsigned char*) cdb, + .cmd_len = sizeof (cdb), + .dxferp = buf, + .dxfer_len = buf_len, + .sbp = sense, + .mx_sb_len = sizeof (sense), + .dxfer_direction = SG_DXFER_FROM_DEV, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + ret = ioctl(fd, SG_IO, &io_hdr); + if (ret != 0) + return ret; + } else + return ret; + } + + if (!(sense[0] == 0x72 && desc[0] == 0x9 && desc[1] == 0x0c) && + !(sense[0] == 0x70 && sense[12] == 0x00 && sense[13] == 0x1d)) { + errno = EIO; + return -1; + } + + return 0; +} + +static int disk_identify_packet_device_command( + int fd, + void *buf, + size_t buf_len) { + + uint8_t cdb[16] = { + /* + * ATA Pass-Through 16 byte command, as described in + * + * T10 04-262r8 ATA Command Pass-Through + * + * from http://www.t10.org/ftp/t10/document.04/04-262r8.pdf + */ + [0] = 0x85, /* OPERATION CODE: 16 byte pass through */ + [1] = 4 << 1, /* PROTOCOL: PIO Data-in */ + [2] = 0x2e, /* OFF_LINE=0, CK_COND=1, T_DIR=1, BYT_BLOK=1, T_LENGTH=2 */ + [3] = 0, /* FEATURES */ + [4] = 0, /* FEATURES */ + [5] = 0, /* SECTORS */ + [6] = 1, /* SECTORS */ + [7] = 0, /* LBA LOW */ + [8] = 0, /* LBA LOW */ + [9] = 0, /* LBA MID */ + [10] = 0, /* LBA MID */ + [11] = 0, /* LBA HIGH */ + [12] = 0, /* LBA HIGH */ + [13] = 0, /* DEVICE */ + [14] = 0xA1, /* Command: ATA IDENTIFY PACKET DEVICE */ + [15] = 0, /* CONTROL */ + }; + uint8_t sense[32] = {}; + uint8_t *desc = sense + 8; + struct sg_io_v4 io_v4 = { + .guard = 'Q', + .protocol = BSG_PROTOCOL_SCSI, + .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD, + .request_len = sizeof (cdb), + .request = (uintptr_t) cdb, + .max_response_len = sizeof (sense), + .response = (uintptr_t) sense, + .din_xfer_len = buf_len, + .din_xferp = (uintptr_t) buf, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + int ret; + + ret = ioctl(fd, SG_IO, &io_v4); + if (ret != 0) { + /* could be that the driver doesn't do version 4, try version 3 */ + if (errno == EINVAL) { + struct sg_io_hdr io_hdr = { + .interface_id = 'S', + .cmdp = (unsigned char*) cdb, + .cmd_len = sizeof (cdb), + .dxferp = buf, + .dxfer_len = buf_len, + .sbp = sense, + .mx_sb_len = sizeof (sense), + .dxfer_direction = SG_DXFER_FROM_DEV, + .timeout = COMMAND_TIMEOUT_MSEC, + }; + + ret = ioctl(fd, SG_IO, &io_hdr); + if (ret != 0) + return ret; + } else + return ret; + } + + if (!(sense[0] == 0x72 && desc[0] == 0x9 && desc[1] == 0x0c)) { + errno = EIO; + return -1; + } + + return 0; +} + +/** + * disk_identify_get_string: + * @identify: A block of IDENTIFY data + * @offset_words: Offset of the string to get, in words. + * @dest: Destination buffer for the string. + * @dest_len: Length of destination buffer, in bytes. + * + * Copies the ATA string from @identify located at @offset_words into @dest. + */ +static void disk_identify_get_string( + uint8_t identify[512], + unsigned offset_words, + char *dest, + size_t dest_len) { + + unsigned c1; + unsigned c2; + + while (dest_len > 0) { + c1 = identify[offset_words * 2 + 1]; + c2 = identify[offset_words * 2]; + *dest = c1; + dest++; + *dest = c2; + dest++; + offset_words++; + dest_len -= 2; + } +} + +static void disk_identify_fixup_string( + uint8_t identify[512], + unsigned offset_words, + size_t len) { + disk_identify_get_string(identify, offset_words, + (char *) identify + offset_words * 2, len); +} + +static void disk_identify_fixup_uint16 (uint8_t identify[512], unsigned offset_words) { + uint16_t *p; + + p = (uint16_t *) identify; + p[offset_words] = le16toh (p[offset_words]); +} + +/** + * disk_identify: + * @fd: File descriptor for the block device. + * @out_identify: Return location for IDENTIFY data. + * @out_is_packet_device: Return location for whether returned data is from a IDENTIFY PACKET DEVICE. + * + * Sends the IDENTIFY DEVICE or IDENTIFY PACKET DEVICE command to the + * device represented by @fd. If successful, then the result will be + * copied into @out_identify and @out_is_packet_device. + * + * This routine is based on code from libatasmart, LGPL v2.1. + * + * Returns: 0 if the data was successfully obtained, otherwise + * non-zero with errno set. + */ +static int disk_identify(int fd, + uint8_t out_identify[512], + int *out_is_packet_device) { + int ret; + uint8_t inquiry_buf[36]; + int peripheral_device_type; + int all_nul_bytes; + int n; + int is_packet_device = 0; + + /* init results */ + memzero(out_identify, 512); + + /* If we were to use ATA PASS_THROUGH (12) on an ATAPI device + * we could accidentally blank media. This is because MMC's BLANK + * command has the same op-code (0x61). + * + * To prevent this from happening we bail out if the device + * isn't a Direct Access Block Device, e.g. SCSI type 0x00 + * (CD/DVD devices are type 0x05). So we send a SCSI INQUIRY + * command first... libata is handling this via its SCSI + * emulation layer. + * + * This also ensures that we're actually dealing with a device + * that understands SCSI commands. + * + * (Yes, it is a bit perverse that we're tunneling the ATA + * command through SCSI and relying on the ATA driver + * emulating SCSI well-enough...) + * + * (See commit 160b069c25690bfb0c785994c7c3710289179107 for + * the original bug-fix and see http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=556635 + * for the original bug-report.) + */ + ret = disk_scsi_inquiry_command (fd, inquiry_buf, sizeof (inquiry_buf)); + if (ret != 0) + goto out; + + /* SPC-4, section 6.4.2: Standard INQUIRY data */ + peripheral_device_type = inquiry_buf[0] & 0x1f; + if (peripheral_device_type == 0x05) + { + is_packet_device = 1; + ret = disk_identify_packet_device_command(fd, out_identify, 512); + goto check_nul_bytes; + } + if (!IN_SET(peripheral_device_type, 0x00, 0x14)) { + ret = -1; + errno = EIO; + goto out; + } + + /* OK, now issue the IDENTIFY DEVICE command */ + ret = disk_identify_command(fd, out_identify, 512); + if (ret != 0) + goto out; + + check_nul_bytes: + /* Check if IDENTIFY data is all NUL bytes - if so, bail */ + all_nul_bytes = 1; + for (n = 0; n < 512; n++) { + if (out_identify[n] != '\0') { + all_nul_bytes = 0; + break; + } + } + + if (all_nul_bytes) { + ret = -1; + errno = EIO; + goto out; + } + +out: + if (out_is_packet_device) + *out_is_packet_device = is_packet_device; + return ret; +} + +int main(int argc, char *argv[]) { + struct hd_driveid id; + union { + uint8_t byte[512]; + uint16_t wyde[256]; + } identify; + char model[41]; + char model_enc[256]; + char serial[21]; + char revision[9]; + const char *node = NULL; + int export = 0; + _cleanup_close_ int fd = -1; + uint16_t word; + int is_packet_device = 0; + static const struct option options[] = { + { "export", no_argument, NULL, 'x' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + for (;;) { + int option; + + option = getopt_long(argc, argv, "xh", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'x': + export = 1; + break; + case 'h': + printf("Usage: %s [--export] [--help] <device>\n" + " -x,--export print values as environment keys\n" + " -h,--help print this help text\n\n", + program_invocation_short_name); + return 0; + } + } + + node = argv[optind]; + if (!node) { + log_error("no node specified"); + return 1; + } + + fd = open(node, O_RDONLY|O_NONBLOCK|O_CLOEXEC); + if (fd < 0) { + log_error("unable to open '%s'", node); + return 1; + } + + if (disk_identify(fd, identify.byte, &is_packet_device) == 0) { + /* + * fix up only the fields from the IDENTIFY data that we are going to + * use and copy it into the hd_driveid struct for convenience + */ + disk_identify_fixup_string(identify.byte, 10, 20); /* serial */ + disk_identify_fixup_string(identify.byte, 23, 8); /* fwrev */ + disk_identify_fixup_string(identify.byte, 27, 40); /* model */ + disk_identify_fixup_uint16(identify.byte, 0); /* configuration */ + disk_identify_fixup_uint16(identify.byte, 75); /* queue depth */ + disk_identify_fixup_uint16(identify.byte, 76); /* SATA capabilities */ + disk_identify_fixup_uint16(identify.byte, 82); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 83); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 84); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 85); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 86); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 87); /* command set supported */ + disk_identify_fixup_uint16(identify.byte, 89); /* time required for SECURITY ERASE UNIT */ + disk_identify_fixup_uint16(identify.byte, 90); /* time required for enhanced SECURITY ERASE UNIT */ + disk_identify_fixup_uint16(identify.byte, 91); /* current APM values */ + disk_identify_fixup_uint16(identify.byte, 94); /* current AAM value */ + disk_identify_fixup_uint16(identify.byte, 108); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 109); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 110); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 111); /* WWN */ + disk_identify_fixup_uint16(identify.byte, 128); /* device lock function */ + disk_identify_fixup_uint16(identify.byte, 217); /* nominal media rotation rate */ + memcpy(&id, identify.byte, sizeof id); + } else { + /* If this fails, then try HDIO_GET_IDENTITY */ + if (ioctl(fd, HDIO_GET_IDENTITY, &id) != 0) { + log_debug_errno(errno, "HDIO_GET_IDENTITY failed for '%s': %m", node); + return 2; + } + } + + memcpy(model, id.model, 40); + model[40] = '\0'; + udev_util_encode_string(model, model_enc, sizeof(model_enc)); + util_replace_whitespace((char *) id.model, model, 40); + util_replace_chars(model, NULL); + util_replace_whitespace((char *) id.serial_no, serial, 20); + util_replace_chars(serial, NULL); + util_replace_whitespace((char *) id.fw_rev, revision, 8); + util_replace_chars(revision, NULL); + + if (export) { + /* Set this to convey the disk speaks the ATA protocol */ + printf("ID_ATA=1\n"); + + if ((id.config >> 8) & 0x80) { + /* This is an ATAPI device */ + switch ((id.config >> 8) & 0x1f) { + case 0: + printf("ID_TYPE=cd\n"); + break; + case 1: + printf("ID_TYPE=tape\n"); + break; + case 5: + printf("ID_TYPE=cd\n"); + break; + case 7: + printf("ID_TYPE=optical\n"); + break; + default: + printf("ID_TYPE=generic\n"); + break; + } + } else + printf("ID_TYPE=disk\n"); + printf("ID_BUS=ata\n"); + printf("ID_MODEL=%s\n", model); + printf("ID_MODEL_ENC=%s\n", model_enc); + printf("ID_REVISION=%s\n", revision); + if (serial[0] != '\0') { + printf("ID_SERIAL=%s_%s\n", model, serial); + printf("ID_SERIAL_SHORT=%s\n", serial); + } else + printf("ID_SERIAL=%s\n", model); + + if (id.command_set_1 & (1<<5)) { + printf("ID_ATA_WRITE_CACHE=1\n"); + printf("ID_ATA_WRITE_CACHE_ENABLED=%d\n", (id.cfs_enable_1 & (1<<5)) ? 1 : 0); + } + if (id.command_set_1 & (1<<10)) { + printf("ID_ATA_FEATURE_SET_HPA=1\n"); + printf("ID_ATA_FEATURE_SET_HPA_ENABLED=%d\n", (id.cfs_enable_1 & (1<<10)) ? 1 : 0); + + /* + * TODO: use the READ NATIVE MAX ADDRESS command to get the native max address + * so it is easy to check whether the protected area is in use. + */ + } + if (id.command_set_1 & (1<<3)) { + printf("ID_ATA_FEATURE_SET_PM=1\n"); + printf("ID_ATA_FEATURE_SET_PM_ENABLED=%d\n", (id.cfs_enable_1 & (1<<3)) ? 1 : 0); + } + if (id.command_set_1 & (1<<1)) { + printf("ID_ATA_FEATURE_SET_SECURITY=1\n"); + printf("ID_ATA_FEATURE_SET_SECURITY_ENABLED=%d\n", (id.cfs_enable_1 & (1<<1)) ? 1 : 0); + printf("ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN=%d\n", id.trseuc * 2); + if ((id.cfs_enable_1 & (1<<1))) /* enabled */ { + if (id.dlf & (1<<8)) + printf("ID_ATA_FEATURE_SET_SECURITY_LEVEL=maximum\n"); + else + printf("ID_ATA_FEATURE_SET_SECURITY_LEVEL=high\n"); + } + if (id.dlf & (1<<5)) + printf("ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN=%d\n", id.trsEuc * 2); + if (id.dlf & (1<<4)) + printf("ID_ATA_FEATURE_SET_SECURITY_EXPIRE=1\n"); + if (id.dlf & (1<<3)) + printf("ID_ATA_FEATURE_SET_SECURITY_FROZEN=1\n"); + if (id.dlf & (1<<2)) + printf("ID_ATA_FEATURE_SET_SECURITY_LOCKED=1\n"); + } + if (id.command_set_1 & (1<<0)) { + printf("ID_ATA_FEATURE_SET_SMART=1\n"); + printf("ID_ATA_FEATURE_SET_SMART_ENABLED=%d\n", (id.cfs_enable_1 & (1<<0)) ? 1 : 0); + } + if (id.command_set_2 & (1<<9)) { + printf("ID_ATA_FEATURE_SET_AAM=1\n"); + printf("ID_ATA_FEATURE_SET_AAM_ENABLED=%d\n", (id.cfs_enable_2 & (1<<9)) ? 1 : 0); + printf("ID_ATA_FEATURE_SET_AAM_VENDOR_RECOMMENDED_VALUE=%d\n", id.acoustic >> 8); + printf("ID_ATA_FEATURE_SET_AAM_CURRENT_VALUE=%d\n", id.acoustic & 0xff); + } + if (id.command_set_2 & (1<<5)) { + printf("ID_ATA_FEATURE_SET_PUIS=1\n"); + printf("ID_ATA_FEATURE_SET_PUIS_ENABLED=%d\n", (id.cfs_enable_2 & (1<<5)) ? 1 : 0); + } + if (id.command_set_2 & (1<<3)) { + printf("ID_ATA_FEATURE_SET_APM=1\n"); + printf("ID_ATA_FEATURE_SET_APM_ENABLED=%d\n", (id.cfs_enable_2 & (1<<3)) ? 1 : 0); + if ((id.cfs_enable_2 & (1<<3))) + printf("ID_ATA_FEATURE_SET_APM_CURRENT_VALUE=%d\n", id.CurAPMvalues & 0xff); + } + if (id.command_set_2 & (1<<0)) + printf("ID_ATA_DOWNLOAD_MICROCODE=1\n"); + + /* + * Word 76 indicates the capabilities of a SATA device. A PATA device shall set + * word 76 to 0000h or FFFFh. If word 76 is set to 0000h or FFFFh, then + * the device does not claim compliance with the Serial ATA specification and words + * 76 through 79 are not valid and shall be ignored. + */ + + word = identify.wyde[76]; + if (!IN_SET(word, 0x0000, 0xffff)) { + printf("ID_ATA_SATA=1\n"); + /* + * If bit 2 of word 76 is set to one, then the device supports the Gen2 + * signaling rate of 3.0 Gb/s (see SATA 2.6). + * + * If bit 1 of word 76 is set to one, then the device supports the Gen1 + * signaling rate of 1.5 Gb/s (see SATA 2.6). + */ + if (word & (1<<2)) + printf("ID_ATA_SATA_SIGNAL_RATE_GEN2=1\n"); + if (word & (1<<1)) + printf("ID_ATA_SATA_SIGNAL_RATE_GEN1=1\n"); + } + + /* Word 217 indicates the nominal media rotation rate of the device */ + word = identify.wyde[217]; + if (word == 0x0001) + printf ("ID_ATA_ROTATION_RATE_RPM=0\n"); /* non-rotating e.g. SSD */ + else if (word >= 0x0401 && word <= 0xfffe) + printf ("ID_ATA_ROTATION_RATE_RPM=%d\n", word); + + /* + * Words 108-111 contain a mandatory World Wide Name (WWN) in the NAA IEEE Registered identifier + * format. Word 108 bits (15:12) shall contain 5h, indicating that the naming authority is IEEE. + * All other values are reserved. + */ + word = identify.wyde[108]; + if ((word & 0xf000) == 0x5000) { + uint64_t wwwn; + + wwwn = identify.wyde[108]; + wwwn <<= 16; + wwwn |= identify.wyde[109]; + wwwn <<= 16; + wwwn |= identify.wyde[110]; + wwwn <<= 16; + wwwn |= identify.wyde[111]; + printf("ID_WWN=0x%1$" PRIx64 "\n" + "ID_WWN_WITH_EXTENSION=0x%1$" PRIx64 "\n", + wwwn); + } + + /* from Linux's include/linux/ata.h */ + if (IN_SET(identify.wyde[0], 0x848a, 0x844a) || + (identify.wyde[83] & 0xc004) == 0x4004) + printf("ID_ATA_CFA=1\n"); + } else { + if (serial[0] != '\0') + printf("%s_%s\n", model, serial); + else + printf("%s\n", model); + } + + return 0; +} diff --git a/src/udev/cdrom_id/cdrom_id.c b/src/udev/cdrom_id/cdrom_id.c new file mode 100644 index 0000000..804cc7c --- /dev/null +++ b/src/udev/cdrom_id/cdrom_id.c @@ -0,0 +1,1018 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * cdrom_id - optical drive and media information prober + */ + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <limits.h> +#include <linux/cdrom.h> +#include <scsi/sg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include "log.h" +#include "memory-util.h" +#include "random-util.h" +#include "udev-util.h" + +/* device info */ +static unsigned cd_cd_rom; +static unsigned cd_cd_r; +static unsigned cd_cd_rw; +static unsigned cd_dvd_rom; +static unsigned cd_dvd_r; +static unsigned cd_dvd_rw; +static unsigned cd_dvd_ram; +static unsigned cd_dvd_plus_r; +static unsigned cd_dvd_plus_rw; +static unsigned cd_dvd_plus_r_dl; +static unsigned cd_dvd_plus_rw_dl; +static unsigned cd_bd; +static unsigned cd_bd_r; +static unsigned cd_bd_re; +static unsigned cd_hddvd; +static unsigned cd_hddvd_r; +static unsigned cd_hddvd_rw; +static unsigned cd_mo; +static unsigned cd_mrw; +static unsigned cd_mrw_w; + +/* media info */ +static unsigned cd_media; +static unsigned cd_media_cd_rom; +static unsigned cd_media_cd_r; +static unsigned cd_media_cd_rw; +static unsigned cd_media_dvd_rom; +static unsigned cd_media_dvd_r; +static unsigned cd_media_dvd_rw; +static unsigned cd_media_dvd_rw_ro; /* restricted overwrite mode */ +static unsigned cd_media_dvd_rw_seq; /* sequential mode */ +static unsigned cd_media_dvd_ram; +static unsigned cd_media_dvd_plus_r; +static unsigned cd_media_dvd_plus_rw; +static unsigned cd_media_dvd_plus_r_dl; +static unsigned cd_media_dvd_plus_rw_dl; +static unsigned cd_media_bd; +static unsigned cd_media_bd_r; +static unsigned cd_media_bd_re; +static unsigned cd_media_hddvd; +static unsigned cd_media_hddvd_r; +static unsigned cd_media_hddvd_rw; +static unsigned cd_media_mo; +static unsigned cd_media_mrw; +static unsigned cd_media_mrw_w; + +static const char *cd_media_state = NULL; +static unsigned cd_media_session_next; +static unsigned cd_media_session_count; +static unsigned cd_media_track_count; +static unsigned cd_media_track_count_data; +static unsigned cd_media_track_count_audio; +static unsigned long long int cd_media_session_last_offset; + +#define ERRCODE(s) ((((s)[2] & 0x0F) << 16) | ((s)[12] << 8) | ((s)[13])) +#define SK(errcode) (((errcode) >> 16) & 0xF) +#define ASC(errcode) (((errcode) >> 8) & 0xFF) +#define ASCQ(errcode) ((errcode) & 0xFF) + +static void info_scsi_cmd_err(const char *cmd, int err) { + if (err == -1) + log_debug("%s failed", cmd); + else + log_debug("%s failed with SK=%Xh/ASC=%02Xh/ACQ=%02Xh", cmd, SK(err), ASC(err), ASCQ(err)); +} + +struct scsi_cmd { + struct cdrom_generic_command cgc; + union { + struct request_sense s; + unsigned char u[18]; + } _sense; + struct sg_io_hdr sg_io; +}; + +static void scsi_cmd_init(struct scsi_cmd *cmd) { + memzero(cmd, sizeof(struct scsi_cmd)); + cmd->cgc.quiet = 1; + cmd->cgc.sense = &cmd->_sense.s; + cmd->sg_io.interface_id = 'S'; + cmd->sg_io.mx_sb_len = sizeof(cmd->_sense); + cmd->sg_io.cmdp = cmd->cgc.cmd; + cmd->sg_io.sbp = cmd->_sense.u; + cmd->sg_io.flags = SG_FLAG_LUN_INHIBIT | SG_FLAG_DIRECT_IO; +} + +static void scsi_cmd_set(struct scsi_cmd *cmd, size_t i, unsigned char arg) { + cmd->sg_io.cmd_len = i + 1; + cmd->cgc.cmd[i] = arg; +} + +#define CHECK_CONDITION 0x01 + +static int scsi_cmd_run(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize) { + int ret = 0; + + if (bufsize > 0) { + cmd->sg_io.dxferp = buf; + cmd->sg_io.dxfer_len = bufsize; + cmd->sg_io.dxfer_direction = SG_DXFER_FROM_DEV; + } else + cmd->sg_io.dxfer_direction = SG_DXFER_NONE; + + if (ioctl(fd, SG_IO, &cmd->sg_io)) + return -1; + + if ((cmd->sg_io.info & SG_INFO_OK_MASK) != SG_INFO_OK) { + errno = EIO; + ret = -1; + if (cmd->sg_io.masked_status & CHECK_CONDITION) { + ret = ERRCODE(cmd->_sense.u); + if (ret == 0) + ret = -1; + } + } + return ret; +} + +static int media_lock(int fd, bool lock) { + int err; + + /* disable the kernel's lock logic */ + err = ioctl(fd, CDROM_CLEAR_OPTIONS, CDO_LOCK); + if (err < 0) + log_debug("CDROM_CLEAR_OPTIONS, CDO_LOCK failed"); + + err = ioctl(fd, CDROM_LOCKDOOR, lock ? 1 : 0); + if (err < 0) + log_debug("CDROM_LOCKDOOR failed"); + + return err; +} + +static int media_eject(int fd) { + struct scsi_cmd sc; + int err; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x1b); + scsi_cmd_set(&sc, 4, 0x02); + scsi_cmd_set(&sc, 5, 0); + err = scsi_cmd_run(&sc, fd, NULL, 0); + if (err != 0) { + info_scsi_cmd_err("START_STOP_UNIT", err); + return -1; + } + return 0; +} + +static int cd_capability_compat(int fd) { + int capability; + + capability = ioctl(fd, CDROM_GET_CAPABILITY, NULL); + if (capability < 0) + return log_debug_errno(errno, "CDROM_GET_CAPABILITY failed"); + + if (capability & CDC_CD_R) + cd_cd_r = 1; + if (capability & CDC_CD_RW) + cd_cd_rw = 1; + if (capability & CDC_DVD) + cd_dvd_rom = 1; + if (capability & CDC_DVD_R) + cd_dvd_r = 1; + if (capability & CDC_DVD_RAM) + cd_dvd_ram = 1; + if (capability & CDC_MRW) + cd_mrw = 1; + if (capability & CDC_MRW_W) + cd_mrw_w = 1; + return 0; +} + +static int cd_media_compat(int fd) { + if (ioctl(fd, CDROM_DRIVE_STATUS, CDSL_CURRENT) != CDS_DISC_OK) + return log_debug_errno(errno, "CDROM_DRIVE_STATUS != CDS_DISC_OK"); + + cd_media = 1; + return 0; +} + +static int cd_inquiry(int fd) { + struct scsi_cmd sc; + unsigned char inq[128]; + int err; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x12); + scsi_cmd_set(&sc, 4, 36); + scsi_cmd_set(&sc, 5, 0); + err = scsi_cmd_run(&sc, fd, inq, 36); + if (err != 0) { + info_scsi_cmd_err("INQUIRY", err); + return -1; + } + + if ((inq[0] & 0x1F) != 5) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "not an MMC unit"); + + log_debug("INQUIRY: [%.8s][%.16s][%.4s]", inq + 8, inq + 16, inq + 32); + return 0; +} + +static void feature_profile_media(int cur_profile) { + switch (cur_profile) { + case 0x03: + case 0x04: + case 0x05: + log_debug("profile 0x%02x ", cur_profile); + cd_media = 1; + cd_media_mo = 1; + break; + case 0x08: + log_debug("profile 0x%02x media_cd_rom", cur_profile); + cd_media = 1; + cd_media_cd_rom = 1; + break; + case 0x09: + log_debug("profile 0x%02x media_cd_r", cur_profile); + cd_media = 1; + cd_media_cd_r = 1; + break; + case 0x0a: + log_debug("profile 0x%02x media_cd_rw", cur_profile); + cd_media = 1; + cd_media_cd_rw = 1; + break; + case 0x10: + log_debug("profile 0x%02x media_dvd_ro", cur_profile); + cd_media = 1; + cd_media_dvd_rom = 1; + break; + case 0x11: + log_debug("profile 0x%02x media_dvd_r", cur_profile); + cd_media = 1; + cd_media_dvd_r = 1; + break; + case 0x12: + log_debug("profile 0x%02x media_dvd_ram", cur_profile); + cd_media = 1; + cd_media_dvd_ram = 1; + break; + case 0x13: + log_debug("profile 0x%02x media_dvd_rw_ro", cur_profile); + cd_media = 1; + cd_media_dvd_rw = 1; + cd_media_dvd_rw_ro = 1; + break; + case 0x14: + log_debug("profile 0x%02x media_dvd_rw_seq", cur_profile); + cd_media = 1; + cd_media_dvd_rw = 1; + cd_media_dvd_rw_seq = 1; + break; + case 0x1B: + log_debug("profile 0x%02x media_dvd_plus_r", cur_profile); + cd_media = 1; + cd_media_dvd_plus_r = 1; + break; + case 0x1A: + log_debug("profile 0x%02x media_dvd_plus_rw", cur_profile); + cd_media = 1; + cd_media_dvd_plus_rw = 1; + break; + case 0x2A: + log_debug("profile 0x%02x media_dvd_plus_rw_dl", cur_profile); + cd_media = 1; + cd_media_dvd_plus_rw_dl = 1; + break; + case 0x2B: + log_debug("profile 0x%02x media_dvd_plus_r_dl", cur_profile); + cd_media = 1; + cd_media_dvd_plus_r_dl = 1; + break; + case 0x40: + log_debug("profile 0x%02x media_bd", cur_profile); + cd_media = 1; + cd_media_bd = 1; + break; + case 0x41: + case 0x42: + log_debug("profile 0x%02x media_bd_r", cur_profile); + cd_media = 1; + cd_media_bd_r = 1; + break; + case 0x43: + log_debug("profile 0x%02x media_bd_re", cur_profile); + cd_media = 1; + cd_media_bd_re = 1; + break; + case 0x50: + log_debug("profile 0x%02x media_hddvd", cur_profile); + cd_media = 1; + cd_media_hddvd = 1; + break; + case 0x51: + log_debug("profile 0x%02x media_hddvd_r", cur_profile); + cd_media = 1; + cd_media_hddvd_r = 1; + break; + case 0x52: + log_debug("profile 0x%02x media_hddvd_rw", cur_profile); + cd_media = 1; + cd_media_hddvd_rw = 1; + break; + default: + log_debug("profile 0x%02x <ignored>", cur_profile); + break; + } +} + +static int feature_profiles(const unsigned char *profiles, size_t size) { + unsigned i; + + for (i = 0; i+4 <= size; i += 4) { + int profile; + + profile = profiles[i] << 8 | profiles[i+1]; + switch (profile) { + case 0x03: + case 0x04: + case 0x05: + log_debug("profile 0x%02x mo", profile); + cd_mo = 1; + break; + case 0x08: + log_debug("profile 0x%02x cd_rom", profile); + cd_cd_rom = 1; + break; + case 0x09: + log_debug("profile 0x%02x cd_r", profile); + cd_cd_r = 1; + break; + case 0x0A: + log_debug("profile 0x%02x cd_rw", profile); + cd_cd_rw = 1; + break; + case 0x10: + log_debug("profile 0x%02x dvd_rom", profile); + cd_dvd_rom = 1; + break; + case 0x12: + log_debug("profile 0x%02x dvd_ram", profile); + cd_dvd_ram = 1; + break; + case 0x13: + case 0x14: + log_debug("profile 0x%02x dvd_rw", profile); + cd_dvd_rw = 1; + break; + case 0x1B: + log_debug("profile 0x%02x dvd_plus_r", profile); + cd_dvd_plus_r = 1; + break; + case 0x1A: + log_debug("profile 0x%02x dvd_plus_rw", profile); + cd_dvd_plus_rw = 1; + break; + case 0x2A: + log_debug("profile 0x%02x dvd_plus_rw_dl", profile); + cd_dvd_plus_rw_dl = 1; + break; + case 0x2B: + log_debug("profile 0x%02x dvd_plus_r_dl", profile); + cd_dvd_plus_r_dl = 1; + break; + case 0x40: + cd_bd = 1; + log_debug("profile 0x%02x bd", profile); + break; + case 0x41: + case 0x42: + cd_bd_r = 1; + log_debug("profile 0x%02x bd_r", profile); + break; + case 0x43: + cd_bd_re = 1; + log_debug("profile 0x%02x bd_re", profile); + break; + case 0x50: + cd_hddvd = 1; + log_debug("profile 0x%02x hddvd", profile); + break; + case 0x51: + cd_hddvd_r = 1; + log_debug("profile 0x%02x hddvd_r", profile); + break; + case 0x52: + cd_hddvd_rw = 1; + log_debug("profile 0x%02x hddvd_rw", profile); + break; + default: + log_debug("profile 0x%02x <ignored>", profile); + break; + } + } + return 0; +} + +/* returns 0 if media was detected */ +static int cd_profiles_old_mmc(int fd) { + struct scsi_cmd sc; + int err; + + unsigned char header[32]; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x51); + scsi_cmd_set(&sc, 8, sizeof(header)); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, header, sizeof(header)); + if (err != 0) { + info_scsi_cmd_err("READ DISC INFORMATION", err); + if (cd_media == 1) { + log_debug("no current profile, but disc is present; assuming CD-ROM"); + cd_media_cd_rom = 1; + cd_media_track_count = 1; + cd_media_track_count_data = 1; + return 0; + } else + return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM), + "no current profile, assuming no media"); + }; + + cd_media = 1; + + if (header[2] & 16) { + cd_media_cd_rw = 1; + log_debug("profile 0x0a media_cd_rw"); + } else if ((header[2] & 3) < 2 && cd_cd_r) { + cd_media_cd_r = 1; + log_debug("profile 0x09 media_cd_r"); + } else { + cd_media_cd_rom = 1; + log_debug("profile 0x08 media_cd_rom"); + } + return 0; +} + +/* returns 0 if media was detected */ +static int cd_profiles(int fd) { + struct scsi_cmd sc; + unsigned char features[65530]; + unsigned cur_profile = 0; + unsigned len; + unsigned i; + int err; + int ret; + + ret = -1; + + /* First query the current profile */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x46); + scsi_cmd_set(&sc, 8, 8); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, features, 8); + if (err != 0) { + info_scsi_cmd_err("GET CONFIGURATION", err); + /* handle pre-MMC2 drives which do not support GET CONFIGURATION */ + if (SK(err) == 0x5 && IN_SET(ASC(err), 0x20, 0x24)) { + log_debug("drive is pre-MMC2 and does not support 46h get configuration command"); + log_debug("trying to work around the problem"); + ret = cd_profiles_old_mmc(fd); + } + goto out; + } + + cur_profile = features[6] << 8 | features[7]; + if (cur_profile > 0) { + log_debug("current profile 0x%02x", cur_profile); + feature_profile_media(cur_profile); + ret = 0; /* we have media */ + } else + log_debug("no current profile, assuming no media"); + + len = features[0] << 24 | features[1] << 16 | features[2] << 8 | features[3]; + log_debug("GET CONFIGURATION: size of features buffer 0x%04x", len); + + if (len > sizeof(features)) { + log_debug("cannot get features in a single query, truncating"); + len = sizeof(features); + } else if (len <= 8) + len = sizeof(features); + + /* Now get the full feature buffer */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x46); + scsi_cmd_set(&sc, 7, ( len >> 8 ) & 0xff); + scsi_cmd_set(&sc, 8, len & 0xff); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, features, len); + if (err != 0) { + info_scsi_cmd_err("GET CONFIGURATION", err); + return -1; + } + + /* parse the length once more, in case the drive decided to have other features suddenly :) */ + len = features[0] << 24 | features[1] << 16 | features[2] << 8 | features[3]; + log_debug("GET CONFIGURATION: size of features buffer 0x%04x", len); + + if (len > sizeof(features)) { + log_debug("cannot get features in a single query, truncating"); + len = sizeof(features); + } + + /* device features */ + for (i = 8; i+4 < len; i += (4 + features[i+3])) { + unsigned feature; + + feature = features[i] << 8 | features[i+1]; + + switch (feature) { + case 0x00: + log_debug("GET CONFIGURATION: feature 'profiles', with %i entries", features[i+3] / 4); + feature_profiles(&features[i]+4, MIN(features[i+3], len - i - 4)); + break; + default: + log_debug("GET CONFIGURATION: feature 0x%04x <ignored>, with 0x%02x bytes", feature, features[i+3]); + break; + } + } +out: + return ret; +} + +static int cd_media_info(int fd) { + struct scsi_cmd sc; + unsigned char header[32]; + static const char *const media_status[] = { + "blank", + "appendable", + "complete", + "other" + }; + int err; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x51); + scsi_cmd_set(&sc, 8, sizeof(header) & 0xff); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, header, sizeof(header)); + if (err != 0) { + info_scsi_cmd_err("READ DISC INFORMATION", err); + return -1; + }; + + cd_media = 1; + log_debug("disk type %02x", header[8]); + log_debug("hardware reported media status: %s", media_status[header[2] & 3]); + + /* exclude plain CDROM, some fake cdroms return 0 for "blank" media here */ + if (!cd_media_cd_rom) + cd_media_state = media_status[header[2] & 3]; + + /* fresh DVD-RW in restricted overwrite mode reports itself as + * "appendable"; change it to "blank" to make it consistent with what + * gets reported after blanking, and what userspace expects */ + if (cd_media_dvd_rw_ro && (header[2] & 3) == 1) + cd_media_state = media_status[0]; + + /* DVD+RW discs (and DVD-RW in restricted mode) once formatted are + * always "complete", DVD-RAM are "other" or "complete" if the disc is + * write protected; we need to check the contents if it is blank */ + if ((cd_media_dvd_rw_ro || cd_media_dvd_plus_rw || cd_media_dvd_plus_rw_dl || cd_media_dvd_ram) && (header[2] & 3) > 1) { + unsigned char buffer[32 * 2048]; + unsigned char len; + int offset; + + if (cd_media_dvd_ram) { + /* a write protected dvd-ram may report "complete" status */ + + unsigned char dvdstruct[8]; + unsigned char format[12]; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0xAD); + scsi_cmd_set(&sc, 7, 0xC0); + scsi_cmd_set(&sc, 9, sizeof(dvdstruct)); + scsi_cmd_set(&sc, 11, 0); + err = scsi_cmd_run(&sc, fd, dvdstruct, sizeof(dvdstruct)); + if (err != 0) { + info_scsi_cmd_err("READ DVD STRUCTURE", err); + return -1; + } + if (dvdstruct[4] & 0x02) { + cd_media_state = media_status[2]; + log_debug("write-protected DVD-RAM media inserted"); + goto determined; + } + + /* let's make sure we don't try to read unformatted media */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x23); + scsi_cmd_set(&sc, 8, sizeof(format)); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, format, sizeof(format)); + if (err != 0) { + info_scsi_cmd_err("READ DVD FORMAT CAPACITIES", err); + return -1; + } + + len = format[3]; + if (len & 7 || len < 16) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "invalid format capacities length"); + + switch(format[8] & 3) { + case 1: + log_debug("unformatted DVD-RAM media inserted"); + /* This means that last format was interrupted + * or failed, blank dvd-ram discs are factory + * formatted. Take no action here as it takes + * quite a while to reformat a dvd-ram and it's + * not automatically started */ + goto determined; + + case 2: + log_debug("formatted DVD-RAM media inserted"); + break; + + case 3: + cd_media = 0; //return no media + return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM), + "format capacities returned no media"); + } + } + + /* Take a closer look at formatted media (unformatted DVD+RW + * has "blank" status", DVD-RAM was examined earlier) and check + * for ISO and UDF PVDs or a fs superblock presence and do it + * in one ioctl (we need just sectors 0 and 16) */ + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x28); + scsi_cmd_set(&sc, 5, 0); + scsi_cmd_set(&sc, 8, 32); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, buffer, sizeof(buffer)); + if (err != 0) { + cd_media = 0; + info_scsi_cmd_err("READ FIRST 32 BLOCKS", err); + return -1; + } + + /* if any non-zero data is found in sector 16 (iso and udf) or + * eventually 0 (fat32 boot sector, ext2 superblock, etc), disc + * is assumed non-blank */ + + for (offset = 32768; offset < (32768 + 2048); offset++) { + if (buffer [offset]) { + log_debug("data in block 16, assuming complete"); + goto determined; + } + } + + for (offset = 0; offset < 2048; offset++) { + if (buffer [offset]) { + log_debug("data in block 0, assuming complete"); + goto determined; + } + } + + cd_media_state = media_status[0]; + log_debug("no data in blocks 0 or 16, assuming blank"); + } + +determined: + /* "other" is e. g. DVD-RAM, can't append sessions there; DVDs in + * restricted overwrite mode can never append, only in sequential mode */ + if ((header[2] & 3) < 2 && !cd_media_dvd_rw_ro) + cd_media_session_next = header[10] << 8 | header[5]; + cd_media_session_count = header[9] << 8 | header[4]; + cd_media_track_count = header[11] << 8 | header[6]; + + return 0; +} + +static int cd_media_toc(int fd) { + struct scsi_cmd sc; + unsigned char header[12]; + unsigned char toc[65536]; + unsigned len, i, num_tracks; + unsigned char *p; + int err; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x43); + scsi_cmd_set(&sc, 6, 1); + scsi_cmd_set(&sc, 8, sizeof(header) & 0xff); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, header, sizeof(header)); + if (err != 0) { + info_scsi_cmd_err("READ TOC", err); + return -1; + } + + len = (header[0] << 8 | header[1]) + 2; + log_debug("READ TOC: len: %d, start track: %d, end track: %d", len, header[2], header[3]); + if (len > sizeof(toc)) + return -1; + if (len < 2) + return -1; + /* 2: first track, 3: last track */ + num_tracks = header[3] - header[2] + 1; + + /* empty media has no tracks */ + if (len < 8) + return 0; + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x43); + scsi_cmd_set(&sc, 6, header[2]); /* First Track/Session Number */ + scsi_cmd_set(&sc, 7, (len >> 8) & 0xff); + scsi_cmd_set(&sc, 8, len & 0xff); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, toc, len); + if (err != 0) { + info_scsi_cmd_err("READ TOC (tracks)", err); + return -1; + } + + /* Take care to not iterate beyond the last valid track as specified in + * the TOC, but also avoid going beyond the TOC length, just in case + * the last track number is invalidly large */ + for (p = toc+4, i = 4; i < len-8 && num_tracks > 0; i += 8, p += 8, --num_tracks) { + unsigned block; + unsigned is_data_track; + + is_data_track = (p[1] & 0x04) != 0; + + block = p[4] << 24 | p[5] << 16 | p[6] << 8 | p[7]; + log_debug("track=%u info=0x%x(%s) start_block=%u", + p[2], p[1] & 0x0f, is_data_track ? "data":"audio", block); + + if (is_data_track) + cd_media_track_count_data++; + else + cd_media_track_count_audio++; + } + + scsi_cmd_init(&sc); + scsi_cmd_set(&sc, 0, 0x43); + scsi_cmd_set(&sc, 2, 1); /* Session Info */ + scsi_cmd_set(&sc, 8, sizeof(header)); + scsi_cmd_set(&sc, 9, 0); + err = scsi_cmd_run(&sc, fd, header, sizeof(header)); + if (err != 0) { + info_scsi_cmd_err("READ TOC (multi session)", err); + return -1; + } + len = header[4+4] << 24 | header[4+5] << 16 | header[4+6] << 8 | header[4+7]; + log_debug("last track %u starts at block %u", header[4+2], len); + cd_media_session_last_offset = (unsigned long long int)len * 2048; + return 0; +} + +int main(int argc, char *argv[]) { + static const struct option options[] = { + { "lock-media", no_argument, NULL, 'l' }, + { "unlock-media", no_argument, NULL, 'u' }, + { "eject-media", no_argument, NULL, 'e' }, + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + bool eject = false; + bool lock = false; + bool unlock = false; + const char *node = NULL; + int fd = -1; + int cnt; + int rc = 0; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + for (;;) { + int option; + + option = getopt_long(argc, argv, "deluh", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'l': + lock = true; + break; + case 'u': + unlock = true; + break; + case 'e': + eject = true; + break; + case 'd': + log_set_target(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_DEBUG); + log_open(); + break; + case 'h': + printf("Usage: %s [options] <device>\n" + " -l,--lock-media lock the media (to enable eject request events)\n" + " -u,--unlock-media unlock the media\n" + " -e,--eject-media eject the media\n" + " -d,--debug debug to stderr\n" + " -h,--help print this help text\n\n", + program_invocation_short_name); + goto exit; + default: + rc = 1; + goto exit; + } + } + + node = argv[optind]; + if (!node) { + log_error("no device"); + rc = 1; + goto exit; + } + + initialize_srand(); + for (cnt = 20; cnt > 0; cnt--) { + struct timespec duration; + + fd = open(node, O_RDONLY|O_NONBLOCK|O_CLOEXEC); + if (fd >= 0 || errno != EBUSY) + break; + duration.tv_sec = 0; + duration.tv_nsec = (100 * 1000 * 1000) + (rand() % 100 * 1000 * 1000); + nanosleep(&duration, NULL); + } + if (fd < 0) { + log_debug("unable to open '%s'", node); + rc = 1; + goto exit; + } + log_debug("probing: '%s'", node); + + /* same data as original cdrom_id */ + if (cd_capability_compat(fd) < 0) { + rc = 1; + goto exit; + } + + /* check for media - don't bail if there's no media as we still need to + * to read profiles */ + cd_media_compat(fd); + + /* check if drive talks MMC */ + if (cd_inquiry(fd) < 0) + goto work; + + /* read drive and possibly current profile */ + if (cd_profiles(fd) != 0) + goto work; + + /* at this point we are guaranteed to have media in the drive - find out more about it */ + + /* get session/track info */ + cd_media_toc(fd); + + /* get writable media state */ + cd_media_info(fd); + +work: + /* lock the media, so we enable eject button events */ + if (lock && cd_media) { + log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (lock)"); + media_lock(fd, true); + } + + if (unlock && cd_media) { + log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)"); + media_lock(fd, false); + } + + if (eject) { + log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)"); + media_lock(fd, false); + log_debug("START_STOP_UNIT (eject)"); + media_eject(fd); + } + + printf("ID_CDROM=1\n"); + if (cd_cd_rom) + printf("ID_CDROM_CD=1\n"); + if (cd_cd_r) + printf("ID_CDROM_CD_R=1\n"); + if (cd_cd_rw) + printf("ID_CDROM_CD_RW=1\n"); + if (cd_dvd_rom) + printf("ID_CDROM_DVD=1\n"); + if (cd_dvd_r) + printf("ID_CDROM_DVD_R=1\n"); + if (cd_dvd_rw) + printf("ID_CDROM_DVD_RW=1\n"); + if (cd_dvd_ram) + printf("ID_CDROM_DVD_RAM=1\n"); + if (cd_dvd_plus_r) + printf("ID_CDROM_DVD_PLUS_R=1\n"); + if (cd_dvd_plus_rw) + printf("ID_CDROM_DVD_PLUS_RW=1\n"); + if (cd_dvd_plus_r_dl) + printf("ID_CDROM_DVD_PLUS_R_DL=1\n"); + if (cd_dvd_plus_rw_dl) + printf("ID_CDROM_DVD_PLUS_RW_DL=1\n"); + if (cd_bd) + printf("ID_CDROM_BD=1\n"); + if (cd_bd_r) + printf("ID_CDROM_BD_R=1\n"); + if (cd_bd_re) + printf("ID_CDROM_BD_RE=1\n"); + if (cd_hddvd) + printf("ID_CDROM_HDDVD=1\n"); + if (cd_hddvd_r) + printf("ID_CDROM_HDDVD_R=1\n"); + if (cd_hddvd_rw) + printf("ID_CDROM_HDDVD_RW=1\n"); + if (cd_mo) + printf("ID_CDROM_MO=1\n"); + if (cd_mrw) + printf("ID_CDROM_MRW=1\n"); + if (cd_mrw_w) + printf("ID_CDROM_MRW_W=1\n"); + + if (cd_media) + printf("ID_CDROM_MEDIA=1\n"); + if (cd_media_mo) + printf("ID_CDROM_MEDIA_MO=1\n"); + if (cd_media_mrw) + printf("ID_CDROM_MEDIA_MRW=1\n"); + if (cd_media_mrw_w) + printf("ID_CDROM_MEDIA_MRW_W=1\n"); + if (cd_media_cd_rom) + printf("ID_CDROM_MEDIA_CD=1\n"); + if (cd_media_cd_r) + printf("ID_CDROM_MEDIA_CD_R=1\n"); + if (cd_media_cd_rw) + printf("ID_CDROM_MEDIA_CD_RW=1\n"); + if (cd_media_dvd_rom) + printf("ID_CDROM_MEDIA_DVD=1\n"); + if (cd_media_dvd_r) + printf("ID_CDROM_MEDIA_DVD_R=1\n"); + if (cd_media_dvd_ram) + printf("ID_CDROM_MEDIA_DVD_RAM=1\n"); + if (cd_media_dvd_rw) + printf("ID_CDROM_MEDIA_DVD_RW=1\n"); + if (cd_media_dvd_plus_r) + printf("ID_CDROM_MEDIA_DVD_PLUS_R=1\n"); + if (cd_media_dvd_plus_rw) + printf("ID_CDROM_MEDIA_DVD_PLUS_RW=1\n"); + if (cd_media_dvd_plus_rw_dl) + printf("ID_CDROM_MEDIA_DVD_PLUS_RW_DL=1\n"); + if (cd_media_dvd_plus_r_dl) + printf("ID_CDROM_MEDIA_DVD_PLUS_R_DL=1\n"); + if (cd_media_bd) + printf("ID_CDROM_MEDIA_BD=1\n"); + if (cd_media_bd_r) + printf("ID_CDROM_MEDIA_BD_R=1\n"); + if (cd_media_bd_re) + printf("ID_CDROM_MEDIA_BD_RE=1\n"); + if (cd_media_hddvd) + printf("ID_CDROM_MEDIA_HDDVD=1\n"); + if (cd_media_hddvd_r) + printf("ID_CDROM_MEDIA_HDDVD_R=1\n"); + if (cd_media_hddvd_rw) + printf("ID_CDROM_MEDIA_HDDVD_RW=1\n"); + + if (cd_media_state) + printf("ID_CDROM_MEDIA_STATE=%s\n", cd_media_state); + if (cd_media_session_next > 0) + printf("ID_CDROM_MEDIA_SESSION_NEXT=%u\n", cd_media_session_next); + if (cd_media_session_count > 0) + printf("ID_CDROM_MEDIA_SESSION_COUNT=%u\n", cd_media_session_count); + if (cd_media_session_count > 1 && cd_media_session_last_offset > 0) + printf("ID_CDROM_MEDIA_SESSION_LAST_OFFSET=%llu\n", cd_media_session_last_offset); + if (cd_media_track_count > 0) + printf("ID_CDROM_MEDIA_TRACK_COUNT=%u\n", cd_media_track_count); + if (cd_media_track_count_audio > 0) + printf("ID_CDROM_MEDIA_TRACK_COUNT_AUDIO=%u\n", cd_media_track_count_audio); + if (cd_media_track_count_data > 0) + printf("ID_CDROM_MEDIA_TRACK_COUNT_DATA=%u\n", cd_media_track_count_data); +exit: + if (fd >= 0) + close(fd); + log_close(); + return rc; +} diff --git a/src/udev/fido_id/fido_id.c b/src/udev/fido_id/fido_id.c new file mode 100644 index 0000000..f14b81d --- /dev/null +++ b/src/udev/fido_id/fido_id.c @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on the usage declared in their report + * descriptor and outputs suitable environment variables. + * + * Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' + */ + +#include <errno.h> +#include <fcntl.h> +#include <linux/hid.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <unistd.h> + +#include "device-internal.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "fido_id_desc.h" +#include "log.h" +#include "macro.h" +#include "main-func.h" +#include "path-util.h" +#include "string-util.h" +#include "udev-util.h" + +static int run(int argc, char **argv) { + _cleanup_(sd_device_unrefp) struct sd_device *device = NULL; + _cleanup_free_ char *desc_path = NULL; + _cleanup_close_ int fd = -1; + + struct sd_device *hid_device; + const char *sys_path; + uint8_t desc[HID_MAX_DESCRIPTOR_SIZE]; + ssize_t desc_len; + + int r; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + if (argc > 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Usage: %s [SYSFS_PATH]", program_invocation_short_name); + + if (argc == 1) { + r = device_new_from_strv(&device, environ); + if (r < 0) + return log_error_errno(r, "Failed to get current device from environment: %m"); + } else { + r = sd_device_new_from_syspath(&device, argv[1]); + if (r < 0) + return log_error_errno(r, "Failed to get device from syspath: %m"); + } + + r = sd_device_get_parent(device, &hid_device); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get parent HID device: %m"); + + r = sd_device_get_syspath(hid_device, &sys_path); + if (r < 0) + return log_device_error_errno(hid_device, r, "Failed to get syspath for HID device: %m"); + + desc_path = path_join(sys_path, "report_descriptor"); + if (!desc_path) + return log_oom(); + + fd = open(desc_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return log_device_error_errno(hid_device, errno, + "Failed to open report descriptor at '%s': %m", desc_path); + + desc_len = read(fd, desc, sizeof(desc)); + if (desc_len < 0) + return log_device_error_errno(hid_device, errno, + "Failed to read report descriptor at '%s': %m", desc_path); + if (desc_len == 0) + return log_device_debug_errno(hid_device, SYNTHETIC_ERRNO(EINVAL), + "Empty report descriptor at '%s'.", desc_path); + + r = is_fido_security_token_desc(desc, desc_len); + if (r < 0) + return log_device_debug_errno(hid_device, r, + "Failed to parse report descriptor at '%s'.", desc_path); + if (r > 0) { + printf("ID_FIDO_TOKEN=1\n"); + printf("ID_SECURITY_TOKEN=1\n"); + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/fido_id/fido_id_desc.c b/src/udev/fido_id/fido_id_desc.c new file mode 100644 index 0000000..2dfa759 --- /dev/null +++ b/src/udev/fido_id/fido_id_desc.c @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' */ + +#include <errno.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#include "fido_id_desc.h" + +#define HID_RPTDESC_FIRST_BYTE_LONG_ITEM 0xfeu +#define HID_RPTDESC_TYPE_GLOBAL 0x1u +#define HID_RPTDESC_TYPE_LOCAL 0x2u +#define HID_RPTDESC_TAG_USAGE_PAGE 0x0u +#define HID_RPTDESC_TAG_USAGE 0x0u + +/* + * HID usage for FIDO CTAP1 ("U2F") and CTAP2 security tokens. + * https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-u2f_hid.h-v1.0-ps-20141009.txt + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#usb-discovery + * https://www.usb.org/sites/default/files/hutrr48.pdf + */ +#define FIDO_FULL_USAGE_CTAPHID 0xf1d00001u + +/* + * Parses a HID report descriptor and identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on their + * declared usage. + * A positive return value indicates that the report descriptor belongs to a FIDO security token. + * https://www.usb.org/sites/default/files/documents/hid1_11.pdf (Section 6.2.2) + */ +int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len) { + uint32_t usage = 0; + + for (size_t pos = 0; pos < desc_len; ) { + uint8_t tag, type, size_code; + size_t size; + uint32_t value; + + /* Report descriptors consists of short items (1-5 bytes) and long items (3-258 bytes). */ + if (desc[pos] == HID_RPTDESC_FIRST_BYTE_LONG_ITEM) { + /* No long items are defined in the spec; skip them. + * The length of the data in a long item is contained in the byte after the long + * item tag. The header consists of three bytes: special long item tag, length, + * actual tag. */ + if (pos + 1 >= desc_len) + return -EINVAL; + pos += desc[pos + 1] + 3; + continue; + } + + /* The first byte of a short item encodes tag, type and size. */ + tag = desc[pos] >> 4; /* Bits 7 to 4 */ + type = (desc[pos] >> 2) & 0x3; /* Bits 3 and 2 */ + size_code = desc[pos] & 0x3; /* Bits 1 and 0 */ + /* Size is coded as follows: + * 0 -> 0 bytes, 1 -> 1 byte, 2 -> 2 bytes, 3 -> 4 bytes + */ + size = size_code < 3 ? size_code : 4; + /* Consume header byte. */ + pos++; + + /* Extract the item value coded on size bytes. */ + if (pos + size > desc_len) + return -EINVAL; + value = 0; + for (size_t i = 0; i < size; i++) + value |= (uint32_t) desc[pos + i] << (8 * i); + /* Consume value bytes. */ + pos += size; + + if (type == HID_RPTDESC_TYPE_GLOBAL && tag == HID_RPTDESC_TAG_USAGE_PAGE) { + /* A usage page is a 16 bit value coded on at most 16 bits. */ + if (size > 2) + return -EINVAL; + /* A usage page sets the upper 16 bits of a following usage. */ + usage = (value & 0x0000ffffu) << 16; + } + + if (type == HID_RPTDESC_TYPE_LOCAL && tag == HID_RPTDESC_TAG_USAGE) { + /* A usage is a 32 bit value, but is prepended with the current usage page if + * coded on less than 4 bytes (that is, at most 2 bytes). */ + if (size == 4) + usage = value; + else + usage = (usage & 0xffff0000u) | (value & 0x0000ffffu); + if (usage == FIDO_FULL_USAGE_CTAPHID) + return 1; + } + } + + return 0; +} diff --git a/src/udev/fido_id/fido_id_desc.h b/src/udev/fido_id/fido_id_desc.h new file mode 100644 index 0000000..57af57e --- /dev/null +++ b/src/udev/fido_id/fido_id_desc.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include <stddef.h> +#include <stdint.h> + +int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len); diff --git a/src/udev/fido_id/fuzz-fido-id-desc.c b/src/udev/fido_id/fuzz-fido-id-desc.c new file mode 100644 index 0000000..44d66df --- /dev/null +++ b/src/udev/fido_id/fuzz-fido-id-desc.c @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/hid.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +#include "fido_id_desc.h" +#include "fuzz.h" +#include "log.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + /* We don't want to fill the logs with messages about parse errors. + * Disable most logging if not running standalone */ + if (!getenv("SYSTEMD_LOG_LEVEL")) + log_set_max_level(LOG_CRIT); + + if (size > HID_MAX_DESCRIPTOR_SIZE) + return 0; + (void) is_fido_security_token_desc(data, size); + + return 0; +} diff --git a/src/udev/fido_id/test-fido-id-desc.c b/src/udev/fido_id/test-fido-id-desc.c new file mode 100644 index 0000000..6836bca --- /dev/null +++ b/src/udev/fido_id/test-fido-id-desc.c @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdint.h> +#include <stdlib.h> + +#include "fido_id_desc.h" +#include "macro.h" + +static void test_is_fido_security_token_desc__fido(void) { + static const uint8_t FIDO_HID_DESC_1[] = { + 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(FIDO_HID_DESC_1, sizeof(FIDO_HID_DESC_1)) > 0); + + static const uint8_t FIDO_HID_DESC_2[] = { + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, + 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, + 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, + 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, + 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0, + 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(FIDO_HID_DESC_2, sizeof(FIDO_HID_DESC_2)) > 0); +} + +static void test_is_fido_security_token_desc__non_fido(void) { + /* Wrong usage page */ + static const uint8_t NON_FIDO_HID_DESC_1[] = { + 0x06, 0xd0, 0xf0, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_1, sizeof(NON_FIDO_HID_DESC_1)) == 0); + + /* Wrong usage */ + static const uint8_t NON_FIDO_HID_DESC_2[] = { + 0x06, 0xd0, 0xf1, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, + 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, + 0x40, 0x91, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_2, sizeof(NON_FIDO_HID_DESC_2)) == 0); + + static const uint8_t NON_FIDO_HID_DESC_3[] = { + 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25, + 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, + 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, + 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, + 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0, + }; + assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_3, sizeof(NON_FIDO_HID_DESC_3)) == 0); +} + +static void test_is_fido_security_token_desc__invalid(void) { + /* Size coded on 1 byte, but no byte given */ + static const uint8_t INVALID_HID_DESC_1[] = { 0x01 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_1, sizeof(INVALID_HID_DESC_1)) < 0); + + /* Size coded on 2 bytes, but only 1 byte given */ + static const uint8_t INVALID_HID_DESC_2[] = { 0x02, 0x01 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_2, sizeof(INVALID_HID_DESC_2)) < 0); + + /* Size coded on 4 bytes, but only 3 bytes given */ + static const uint8_t INVALID_HID_DESC_3[] = { 0x03, 0x01, 0x02, 0x03 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_3, sizeof(INVALID_HID_DESC_3)) < 0); + + /* Long item without a size byte */ + static const uint8_t INVALID_HID_DESC_4[] = { 0xfe }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_4, sizeof(INVALID_HID_DESC_4)) < 0); + + /* Usage pages are coded on at most 2 bytes */ + static const uint8_t INVALID_HID_DESC_5[] = { 0x07, 0x01, 0x02, 0x03, 0x04 }; + assert_se(is_fido_security_token_desc(INVALID_HID_DESC_5, sizeof(INVALID_HID_DESC_5)) < 0); +} + +int main(int argc, char *argv[]) { + test_is_fido_security_token_desc__fido(); + test_is_fido_security_token_desc__non_fido(); + test_is_fido_security_token_desc__invalid(); + + return EXIT_SUCCESS; +} diff --git a/src/udev/generate-keyboard-keys-gperf.sh b/src/udev/generate-keyboard-keys-gperf.sh new file mode 100755 index 0000000..c78652a --- /dev/null +++ b/src/udev/generate-keyboard-keys-gperf.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +awk ' + BEGIN { + print "%{\n\ +#if __GNUC__ >= 7\n\ +_Pragma(\"GCC diagnostic ignored \\\"-Wimplicit-fallthrough\\\"\")\n\ +#endif\n\ +%}" + print "struct key_name { const char* name; unsigned short id; };" + print "%null-strings" + print "%%" + } + + /^KEY_/ { print tolower(substr($1 ,5)) ", " $1 } + { print tolower($1) ", " $1 } +' < "$1" diff --git a/src/udev/generate-keyboard-keys-list.sh b/src/udev/generate-keyboard-keys-list.sh new file mode 100755 index 0000000..aa00c15 --- /dev/null +++ b/src/udev/generate-keyboard-keys-list.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +$1 -dM -include linux/input.h - </dev/null | awk ' + /\<(KEY_(MAX|MIN_INTERESTING))|(BTN_(MISC|MOUSE|JOYSTICK|GAMEPAD|DIGI|WHEEL|TRIGGER_HAPPY))\>/ { next } + /^#define[ \t]+(KEY|BTN)_[^ ]+[ \t]+[0-9BK]/ { print $2 } +' diff --git a/src/udev/meson.build b/src/udev/meson.build new file mode 100644 index 0000000..5eb0f99 --- /dev/null +++ b/src/udev/meson.build @@ -0,0 +1,225 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +udevadm_sources = files(''' + udevadm.c + udevadm.h + udevadm-control.c + udevadm-hwdb.c + udevadm-info.c + udevadm-monitor.c + udevadm-settle.c + udevadm-test.c + udevadm-test-builtin.c + udevadm-trigger.c + udevadm-util.c + udevadm-util.h + udevd.c +'''.split()) + +libudev_core_sources = ''' + udev-ctrl.c + udev-ctrl.h + udev-event.c + udev-event.h + udev-node.c + udev-node.h + udev-rules.c + udev-rules.h + udev-watch.c + udev-watch.h + udev-builtin.c + udev-builtin.h + udev-builtin-btrfs.c + udev-builtin-hwdb.c + udev-builtin-input_id.c + udev-builtin-keyboard.c + udev-builtin-net_id.c + udev-builtin-net_setup_link.c + udev-builtin-path_id.c + udev-builtin-usb_id.c + net/link-config.c + net/link-config.h +'''.split() + +if conf.get('HAVE_KMOD') == 1 + libudev_core_sources += ['udev-builtin-kmod.c'] +endif + +if conf.get('HAVE_BLKID') == 1 + libudev_core_sources += ['udev-builtin-blkid.c'] +endif + +if conf.get('HAVE_ACL') == 1 + libudev_core_sources += ['udev-builtin-uaccess.c', + logind_acl_c, + sd_login_sources] +endif + +############################################################ + +generate_keyboard_keys_list = find_program('generate-keyboard-keys-list.sh') +keyboard_keys_list_txt = custom_target( + 'keyboard-keys-list.txt', + output : 'keyboard-keys-list.txt', + command : [generate_keyboard_keys_list, cpp], + capture : true) + +generate_keyboard_keys_gperf = find_program('generate-keyboard-keys-gperf.sh') +fname = 'keyboard-keys-from-name.gperf' +gperf_file = custom_target( + fname, + input : keyboard_keys_list_txt, + output : fname, + command : [generate_keyboard_keys_gperf, '@INPUT@'], + capture : true) + +fname = 'keyboard-keys-from-name.h' +keyboard_keys_from_name_h = custom_target( + fname, + input : gperf_file, + output : fname, + command : [gperf, + '-L', 'ANSI-C', '-t', + '-N', 'keyboard_lookup_key', + '-H', 'hash_key_name', + '-p', '-C', + '@INPUT@'], + capture : true) + +############################################################ + +link_config_gperf_c = custom_target( + 'link-config-gperf.c', + input : 'net/link-config-gperf.gperf', + output : 'link-config-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +############################################################ + +if get_option('link-udev-shared') + udev_link_with = [libshared] + udev_rpath = rootlibexecdir +else + udev_link_with = [libshared_static, + libsystemd_static] + udev_rpath = '' +endif + +libudev_basic = static_library( + 'udev-basic', + libudev_sources, + include_directories : includes, + c_args : ['-fvisibility=default']) + +libudev_static = static_library( + 'udev_static', + include_directories : includes, + link_with : udev_link_with, + link_whole : libudev_basic) + +static_libudev = get_option('static-libudev') +static_libudev_pic = static_libudev == 'true' or static_libudev == 'pic' +install_libudev_static = static_library( + 'udev', + basic_sources, + shared_sources, + libsystemd_sources, + libudev_sources, + disable_mempool_c, + include_directories : includes, + build_by_default : static_libudev != 'false', + install : static_libudev != 'false', + install_dir : rootlibdir, + link_depends : libudev_sym, + dependencies : libshared_deps + [libmount], + c_args : static_libudev_pic ? [] : ['-fno-PIC'], + pic : static_libudev_pic) + +libudev = shared_library( + 'udev', + disable_mempool_c, + version : libudev_version, + include_directories : includes, + link_args : ['-shared', + '-Wl,--version-script=' + libudev_sym_path], + link_with : [libsystemd_static, libshared_static], + link_whole : libudev_basic, + dependencies : [threads], + link_depends : libudev_sym, + install : true, + install_dir : rootlibdir) + +libudev_core_includes = [includes, include_directories('net')] +libudev_core = static_library( + 'udev-core', + libudev_core_sources, + link_config_gperf_c, + keyboard_keys_from_name_h, + include_directories : libudev_core_includes, + c_args : ['-DLOG_REALM=LOG_REALM_UDEV'], + link_with : udev_link_with, + dependencies : [libblkid, libkmod]) + +foreach prog : [['ata_id/ata_id.c'], + ['cdrom_id/cdrom_id.c'], + ['fido_id/fido_id.c', + 'fido_id/fido_id_desc.c', + 'fido_id/fido_id_desc.h'], + ['scsi_id/scsi_id.c', + 'scsi_id/scsi_id.h', + 'scsi_id/scsi_serial.c', + 'scsi_id/scsi.h'], + ['v4l_id/v4l_id.c'], + ['mtd_probe/mtd_probe.c', + 'mtd_probe/mtd_probe.h', + 'mtd_probe/probe_smartmedia.c']] + + executable(prog[0].split('/')[0], + prog, + include_directories : includes, + c_args : ['-DLOG_REALM=LOG_REALM_UDEV'], + dependencies : [versiondep], + link_with : [libudev_static], + install_rpath : udev_rpath, + install : true, + install_dir : udevlibexecdir) +endforeach + +if install_sysconfdir + install_data('udev.conf', + install_dir : join_paths(sysconfdir, 'udev')) +endif + +configure_file( + input : 'udev.pc.in', + output : 'udev.pc', + configuration : substs, + install_dir : pkgconfigdatadir == 'no' ? '' : pkgconfigdatadir) + +if install_sysconfdir + meson.add_install_script('sh', '-c', + mkdir_p.format(join_paths(sysconfdir, 'udev/rules.d'))) +endif + +fuzzers += [ + [['src/udev/net/fuzz-link-parser.c', + 'src/fuzz/fuzz.h'], + [libudev_core, + libudev_static, + libsystemd_network, + libshared], + [threads, + libacl]], + + [['src/udev/fido_id/fuzz-fido-id-desc.c', + 'src/udev/fido_id/fido_id_desc.c'], + [], + []], + ] + +tests += [ + [['src/udev/fido_id/test-fido-id-desc.c', + 'src/udev/fido_id/fido_id_desc.c'], + [], + []], + ] diff --git a/src/udev/mtd_probe/mtd_probe.c b/src/udev/mtd_probe/mtd_probe.c new file mode 100644 index 0000000..df1f1c1 --- /dev/null +++ b/src/udev/mtd_probe/mtd_probe.c @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2010 - Maxim Levitsky + * + * mtd_probe is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mtd_probe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mtd_probe; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include <errno.h> +#include <fcntl.h> +#include <mtd/mtd-user.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "fd-util.h" +#include "mtd_probe.h" + +int main(int argc, char** argv) { + _cleanup_close_ int mtd_fd = -1; + mtd_info_t mtd_info; + + if (argc != 2) { + printf("usage: mtd_probe /dev/mtd[n]\n"); + return EXIT_FAILURE; + } + + mtd_fd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (mtd_fd < 0) { + log_error_errno(errno, "Failed to open: %m"); + return EXIT_FAILURE; + } + + if (ioctl(mtd_fd, MEMGETINFO, &mtd_info) < 0) { + log_error_errno(errno, "Failed to issue MEMGETINFO ioctl: %m"); + return EXIT_FAILURE; + } + + if (probe_smart_media(mtd_fd, &mtd_info) < 0) + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} diff --git a/src/udev/mtd_probe/mtd_probe.h b/src/udev/mtd_probe/mtd_probe.h new file mode 100644 index 0000000..ae03a7d --- /dev/null +++ b/src/udev/mtd_probe/mtd_probe.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * Copyright © 2010 - Maxim Levitsky + * + * mtd_probe is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mtd_probe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mtd_probe; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include <mtd/mtd-user.h> + +#include "macro.h" + +/* Full oob structure as written on the flash */ +struct sm_oob { + uint32_t reserved; + uint8_t data_status; + uint8_t block_status; + uint8_t lba_copy1[2]; + uint8_t ecc2[3]; + uint8_t lba_copy2[2]; + uint8_t ecc1[3]; +} _packed_; + +/* one sector is always 512 bytes, but it can consist of two nand pages */ +#define SM_SECTOR_SIZE 512 + +/* oob area is also 16 bytes, but might be from two pages */ +#define SM_OOB_SIZE 16 + +/* This is maximum zone size, and all devices that have more that one zone + have this size */ +#define SM_MAX_ZONE_SIZE 1024 + +/* support for small page nand */ +#define SM_SMALL_PAGE 256 +#define SM_SMALL_OOB_SIZE 8 + +int probe_smart_media(int mtd_fd, mtd_info_t *info); diff --git a/src/udev/mtd_probe/probe_smartmedia.c b/src/udev/mtd_probe/probe_smartmedia.c new file mode 100644 index 0000000..f4612ba --- /dev/null +++ b/src/udev/mtd_probe/probe_smartmedia.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2010 - Maxim Levitsky + * + * mtd_probe is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mtd_probe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mtd_probe; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + */ + +#include <errno.h> +#include <fcntl.h> +#include <mtd/mtd-user.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "mtd_probe.h" + +static const uint8_t cis_signature[] = { + 0x01, 0x03, 0xD9, 0x01, 0xFF, 0x18, 0x02, 0xDF, 0x01, 0x20 +}; + +int probe_smart_media(int mtd_fd, mtd_info_t* info) { + int sector_size; + int block_size; + int size_in_megs; + int spare_count; + _cleanup_free_ uint8_t *cis_buffer = NULL; + int offset; + int cis_found = 0; + + cis_buffer = malloc(SM_SECTOR_SIZE); + if (!cis_buffer) + return log_oom(); + + if (info->type != MTD_NANDFLASH) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Not marked MTD_NANDFLASH."); + + sector_size = info->writesize; + block_size = info->erasesize; + size_in_megs = info->size / (1024 * 1024); + + if (!IN_SET(sector_size, SM_SECTOR_SIZE, SM_SMALL_PAGE)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Unexpected sector size: %i", sector_size); + + switch(size_in_megs) { + case 1: + case 2: + spare_count = 6; + break; + case 4: + spare_count = 12; + break; + default: + spare_count = 24; + break; + } + + for (offset = 0; offset < block_size * spare_count; offset += sector_size) { + (void) lseek(mtd_fd, SEEK_SET, offset); + + if (read(mtd_fd, cis_buffer, SM_SECTOR_SIZE) == SM_SECTOR_SIZE) { + cis_found = 1; + break; + } + } + + if (!cis_found) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "CIS not found"); + + if (memcmp(cis_buffer, cis_signature, sizeof(cis_signature)) != 0 && + memcmp(cis_buffer + SM_SMALL_PAGE, cis_signature, sizeof(cis_signature)) != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "CIS signature didn't match"); + + printf("MTD_FTL=smartmedia\n"); + return 0; +} diff --git a/src/udev/net/fuzz-link-parser.c b/src/udev/net/fuzz-link-parser.c new file mode 100644 index 0000000..6f3469c --- /dev/null +++ b/src/udev/net/fuzz-link-parser.c @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fs-util.h" +#include "fuzz.h" +#include "link-config.h" +#include "tmpfile-util.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(link_config_ctx_freep) link_config_ctx *ctx = NULL; + _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-link-config.XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + + if (size > 65535) + return 0; + + if (!getenv("SYSTEMD_LOG_LEVEL")) + log_set_max_level(LOG_CRIT); + + assert_se(fmkostemp_safe(filename, "r+", &f) == 0); + if (size != 0) + assert_se(fwrite(data, size, 1, f) == 1); + + fflush(f); + assert_se(link_config_ctx_new(&ctx) >= 0); + (void) link_load_one(ctx, filename); + return 0; +} diff --git a/src/udev/net/fuzz-link-parser.options b/src/udev/net/fuzz-link-parser.options new file mode 100644 index 0000000..0824b19 --- /dev/null +++ b/src/udev/net/fuzz-link-parser.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65535 diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf new file mode 100644 index 0000000..20f5d7e --- /dev/null +++ b/src/udev/net/link-config-gperf.gperf @@ -0,0 +1,68 @@ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"") +#endif +#include <stddef.h> +#include "conf-parser.h" +#include "ethtool-util.h" +#include "link-config.h" +#include "network-internal.h" +#include "socket-util.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name link_config_gperf_hash +%define lookup-function-name link_config_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Match.MACAddress, config_parse_hwaddrs, 0, offsetof(link_config, match_mac) +Match.PermanentMACAddress, config_parse_hwaddrs, 0, offsetof(link_config, match_permanent_mac) +Match.OriginalName, config_parse_match_ifnames, 0, offsetof(link_config, match_name) +Match.Path, config_parse_match_strv, 0, offsetof(link_config, match_path) +Match.Driver, config_parse_match_strv, 0, offsetof(link_config, match_driver) +Match.Type, config_parse_match_strv, 0, offsetof(link_config, match_type) +Match.Property, config_parse_match_property, 0, offsetof(link_config, match_property) +Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(link_config, conditions) +Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(link_config, conditions) +Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(link_config, conditions) +Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(link_config, conditions) +Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(link_config, conditions) +Link.Description, config_parse_string, 0, offsetof(link_config, description) +Link.MACAddressPolicy, config_parse_mac_address_policy, 0, offsetof(link_config, mac_address_policy) +Link.MACAddress, config_parse_hwaddr, 0, offsetof(link_config, mac) +Link.NamePolicy, config_parse_name_policy, 0, offsetof(link_config, name_policy) +Link.Name, config_parse_ifname, 0, offsetof(link_config, name) +Link.AlternativeName, config_parse_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(link_config, alternative_names) +Link.AlternativeNamesPolicy, config_parse_alternative_names_policy, 0, offsetof(link_config, alternative_names_policy) +Link.Alias, config_parse_ifalias, 0, offsetof(link_config, alias) +Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(link_config, mtu) +Link.BitsPerSecond, config_parse_si_uint64, 0, offsetof(link_config, speed) +Link.Duplex, config_parse_duplex, 0, offsetof(link_config, duplex) +Link.AutoNegotiation, config_parse_tristate, 0, offsetof(link_config, autonegotiation) +Link.WakeOnLan, config_parse_wol, 0, offsetof(link_config, wol) +Link.Port, config_parse_port, 0, offsetof(link_config, port) +Link.ReceiveChecksumOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_RX]) +Link.TransmitChecksumOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_TX]) +Link.GenericSegmentationOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_GSO]) +Link.TCPSegmentationOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_TSO]) +Link.TCP6SegmentationOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_TSO6]) +Link.UDPSegmentationOffload, config_parse_warn_compat, DISABLED_LEGACY, 0 +Link.GenericReceiveOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_GRO]) +Link.LargeReceiveOffload, config_parse_tristate, 0, offsetof(link_config, features[NET_DEV_FEAT_LRO]) +Link.RxChannels, config_parse_channel, 0, offsetof(link_config, channels) +Link.TxChannels, config_parse_channel, 0, offsetof(link_config, channels) +Link.OtherChannels, config_parse_channel, 0, offsetof(link_config, channels) +Link.CombinedChannels, config_parse_channel, 0, offsetof(link_config, channels) +Link.Advertise, config_parse_advertise, 0, offsetof(link_config, advertise) +Link.RxBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring) +Link.RxMiniBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring) +Link.RxJumboBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring) +Link.TxBufferSize, config_parse_nic_buffer_size, 0, offsetof(link_config, ring) +Link.RxFlowControl, config_parse_tristate, 0, offsetof(link_config, rx_flow_control) +Link.TxFlowControl, config_parse_tristate, 0, offsetof(link_config, tx_flow_control) +Link.AutoNegotiationFlowControl, config_parse_tristate, 0, offsetof(link_config, autoneg_flow_control) diff --git a/src/udev/net/link-config.c b/src/udev/net/link-config.c new file mode 100644 index 0000000..d12fd0e --- /dev/null +++ b/src/udev/net/link-config.c @@ -0,0 +1,711 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/netdevice.h> +#include <netinet/ether.h> +#include <unistd.h> + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "alloc-util.h" +#include "conf-files.h" +#include "conf-parser.h" +#include "def.h" +#include "device-private.h" +#include "device-util.h" +#include "ethtool-util.h" +#include "fd-util.h" +#include "link-config.h" +#include "log.h" +#include "memory-util.h" +#include "netif-naming-scheme.h" +#include "netlink-util.h" +#include "network-internal.h" +#include "parse-util.h" +#include "path-lookup.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "random-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" + +struct link_config_ctx { + LIST_HEAD(link_config, links); + + int ethtool_fd; + + bool enable_name_policy; + + sd_netlink *rtnl; + + usec_t network_dirs_ts_usec; +}; + +static void link_config_free(link_config *link) { + if (!link) + return; + + free(link->filename); + + set_free_free(link->match_mac); + set_free_free(link->match_permanent_mac); + strv_free(link->match_path); + strv_free(link->match_driver); + strv_free(link->match_type); + strv_free(link->match_name); + strv_free(link->match_property); + condition_free_list(link->conditions); + + free(link->description); + free(link->mac); + free(link->name_policy); + free(link->name); + strv_free(link->alternative_names); + free(link->alternative_names_policy); + free(link->alias); + + free(link); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(link_config*, link_config_free); + +static void link_configs_free(link_config_ctx *ctx) { + link_config *link, *link_next; + + if (!ctx) + return; + + LIST_FOREACH_SAFE(links, link, link_next, ctx->links) + link_config_free(link); +} + +void link_config_ctx_free(link_config_ctx *ctx) { + if (!ctx) + return; + + safe_close(ctx->ethtool_fd); + + sd_netlink_unref(ctx->rtnl); + + link_configs_free(ctx); + + free(ctx); + + return; +} + +int link_config_ctx_new(link_config_ctx **ret) { + _cleanup_(link_config_ctx_freep) link_config_ctx *ctx = NULL; + + if (!ret) + return -EINVAL; + + ctx = new0(link_config_ctx, 1); + if (!ctx) + return -ENOMEM; + + LIST_HEAD_INIT(ctx->links); + + ctx->ethtool_fd = -1; + + ctx->enable_name_policy = true; + + *ret = TAKE_PTR(ctx); + + return 0; +} + +int link_load_one(link_config_ctx *ctx, const char *filename) { + _cleanup_(link_config_freep) link_config *link = NULL; + _cleanup_fclose_ FILE *file = NULL; + _cleanup_free_ char *name = NULL; + size_t i; + int r; + + assert(ctx); + assert(filename); + + file = fopen(filename, "re"); + if (!file) + return errno == ENOENT ? 0 : -errno; + + if (null_or_empty_fd(fileno(file))) { + log_debug("Skipping empty file: %s", filename); + return 0; + } + + name = strdup(filename); + if (!name) + return -ENOMEM; + + link = new(link_config, 1); + if (!link) + return -ENOMEM; + + *link = (link_config) { + .filename = TAKE_PTR(name), + .mac_address_policy = _MAC_ADDRESS_POLICY_INVALID, + .wol = _WOL_INVALID, + .duplex = _DUP_INVALID, + .port = _NET_DEV_PORT_INVALID, + .autonegotiation = -1, + .rx_flow_control = -1, + .tx_flow_control = -1, + .autoneg_flow_control = -1, + }; + + for (i = 0; i < ELEMENTSOF(link->features); i++) + link->features[i] = -1; + + r = config_parse(NULL, filename, file, + "Match\0Link\0", + config_item_perf_lookup, link_config_gperf_lookup, + CONFIG_PARSE_WARN, link, + NULL); + if (r < 0) + return r; + + if (set_isempty(link->match_mac) && set_isempty(link->match_permanent_mac) && + strv_isempty(link->match_path) && strv_isempty(link->match_driver) && strv_isempty(link->match_type) && + strv_isempty(link->match_name) && strv_isempty(link->match_property) && !link->conditions) { + log_warning("%s: No valid settings found in the [Match] section, ignoring file. " + "To match all interfaces, add OriginalName=* in the [Match] section.", + filename); + return 0; + } + + if (!condition_test_list(link->conditions, environ, NULL, NULL, NULL)) { + log_debug("%s: Conditions do not match the system environment, skipping.", filename); + return 0; + } + + if (IN_SET(link->mac_address_policy, MAC_ADDRESS_POLICY_PERSISTENT, MAC_ADDRESS_POLICY_RANDOM) && link->mac) { + log_warning("%s: MACAddress= in [Link] section will be ignored when MACAddressPolicy= " + "is set to \"persistent\" or \"random\".", + filename); + link->mac = mfree(link->mac); + } + + log_debug("Parsed configuration file %s", filename); + + LIST_PREPEND(links, ctx->links, TAKE_PTR(link)); + return 0; +} + +static bool enable_name_policy(void) { + bool b; + + return proc_cmdline_get_bool("net.ifnames", &b) <= 0 || b; +} + +static int link_unsigned_attribute(sd_device *device, const char *attr, unsigned *type) { + const char *s; + int r; + + r = sd_device_get_sysattr_value(device, attr, &s); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to query %s: %m", attr); + + r = safe_atou(s, type); + if (r < 0) + return log_device_warning_errno(device, r, "Failed to parse %s \"%s\": %m", attr, s); + + log_device_debug(device, "Device has %s=%u", attr, *type); + return 0; +} + +int link_config_load(link_config_ctx *ctx) { + _cleanup_strv_free_ char **files = NULL; + char **f; + int r; + + link_configs_free(ctx); + + if (!enable_name_policy()) { + ctx->enable_name_policy = false; + log_info("Network interface NamePolicy= disabled on kernel command line, ignoring."); + } + + /* update timestamp */ + paths_check_timestamp(NETWORK_DIRS, &ctx->network_dirs_ts_usec, true); + + r = conf_files_list_strv(&files, ".link", NULL, 0, NETWORK_DIRS); + if (r < 0) + return log_error_errno(r, "failed to enumerate link files: %m"); + + STRV_FOREACH_BACKWARDS(f, files) { + r = link_load_one(ctx, *f); + if (r < 0) + log_error_errno(r, "Failed to load %s, ignoring: %m", *f); + } + + return 0; +} + +bool link_config_should_reload(link_config_ctx *ctx) { + return paths_check_timestamp(NETWORK_DIRS, &ctx->network_dirs_ts_usec, false); +} + +int link_config_get(link_config_ctx *ctx, sd_device *device, link_config **ret) { + unsigned name_assign_type = NET_NAME_UNKNOWN; + struct ether_addr permanent_mac = {}; + unsigned short iftype = 0; + link_config *link; + const char *name; + int ifindex, r; + + assert(ctx); + assert(device); + assert(ret); + + r = sd_device_get_sysname(device, &name); + if (r < 0) + return r; + + r = sd_device_get_ifindex(device, &ifindex); + if (r < 0) + return r; + + r = rtnl_get_link_iftype(&ctx->rtnl, ifindex, &iftype); + if (r < 0) + return r; + + r = ethtool_get_permanent_macaddr(&ctx->ethtool_fd, name, &permanent_mac); + if (r < 0) + log_device_debug_errno(device, r, "Failed to get permanent MAC address, ignoring: %m"); + + (void) link_unsigned_attribute(device, "name_assign_type", &name_assign_type); + + LIST_FOREACH(links, link, ctx->links) { + if (net_match_config(link->match_mac, link->match_permanent_mac, link->match_path, link->match_driver, + link->match_type, link->match_name, link->match_property, NULL, NULL, NULL, + device, NULL, &permanent_mac, NULL, iftype, NULL, NULL, 0, NULL, NULL)) { + + if (link->match_name && !strv_contains(link->match_name, "*") && name_assign_type == NET_NAME_ENUM) + log_device_warning(device, "Config file %s is applied to device based on potentially unpredictable interface name.", + link->filename); + else + log_device_debug(device, "Config file %s is applied", link->filename); + + *ret = link; + return 0; + } + } + + return -ENOENT; +} + +static int link_config_apply_ethtool_settings(int *ethtool_fd, const link_config *config, sd_device *device) { + const char *name; + int r; + + assert(ethtool_fd); + assert(config); + assert(device); + + r = sd_device_get_sysname(device, &name); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get sysname: %m"); + + r = ethtool_set_glinksettings(ethtool_fd, name, + config->autonegotiation, config->advertise, + config->speed, config->duplex, config->port); + if (r < 0) { + if (config->port != _NET_DEV_PORT_INVALID) + log_device_warning_errno(device, r, "Could not set port '%s', ignoring: %m", port_to_string(config->port)); + + if (!eqzero(config->advertise)) + log_device_warning_errno(device, r, "Could not set advertise mode, ignoring: %m"); /* TODO: include modes in the log message. */ + + if (config->speed) { + unsigned speed = DIV_ROUND_UP(config->speed, 1000000); + if (r == -EOPNOTSUPP) { + r = ethtool_set_speed(ethtool_fd, name, speed, config->duplex); + if (r < 0) + log_device_warning_errno(device, r, "Could not set speed to %uMbps, ignoring: %m", speed); + } + } + + if (config->duplex != _DUP_INVALID) + log_device_warning_errno(device, r, "Could not set duplex to %s, ignoring: %m", duplex_to_string(config->duplex)); + } + + r = ethtool_set_wol(ethtool_fd, name, config->wol); + if (r < 0) + log_device_warning_errno(device, r, "Could not set WakeOnLan to %s, ignoring: %m", wol_to_string(config->wol)); + + r = ethtool_set_features(ethtool_fd, name, config->features); + if (r < 0) + log_device_warning_errno(device, r, "Could not set offload features, ignoring: %m"); + + if (config->channels.rx_count_set || config->channels.tx_count_set || config->channels.other_count_set || config->channels.combined_count_set) { + r = ethtool_set_channels(ethtool_fd, name, &config->channels); + if (r < 0) + log_device_warning_errno(device, r, "Could not set channels, ignoring: %m"); + } + + if (config->ring.rx_pending_set || config->ring.rx_mini_pending_set || config->ring.rx_jumbo_pending_set || config->ring.tx_pending_set) { + r = ethtool_set_nic_buffer_size(ethtool_fd, name, &config->ring); + if (r < 0) + log_device_warning_errno(device, r, "Could not set ring buffer, ignoring: %m"); + } + + if (config->rx_flow_control >= 0 || config->tx_flow_control >= 0 || config->autoneg_flow_control >= 0) { + r = ethtool_set_flow_control(ethtool_fd, name, config->rx_flow_control, config->tx_flow_control, config->autoneg_flow_control); + if (r < 0) + log_device_warning_errno(device, r, "Could not set flow control, ignoring: %m"); + } + + return 0; +} + +static int get_mac(sd_device *device, MACAddressPolicy policy, struct ether_addr *mac) { + unsigned addr_type; + bool want_random = policy == MAC_ADDRESS_POLICY_RANDOM; + int r; + + assert(IN_SET(policy, MAC_ADDRESS_POLICY_RANDOM, MAC_ADDRESS_POLICY_PERSISTENT)); + + r = link_unsigned_attribute(device, "addr_assign_type", &addr_type); + if (r < 0) + return r; + switch (addr_type) { + case NET_ADDR_SET: + return log_device_debug(device, "MAC on the device already set by userspace"); + case NET_ADDR_STOLEN: + return log_device_debug(device, "MAC on the device already set based on another device"); + case NET_ADDR_RANDOM: + case NET_ADDR_PERM: + break; + default: + log_device_warning(device, "Unknown addr_assign_type %u, ignoring", addr_type); + return 0; + } + + if (want_random == (addr_type == NET_ADDR_RANDOM)) + return log_device_debug(device, "MAC on the device already matches policy *%s*", + mac_address_policy_to_string(policy)); + + if (want_random) { + log_device_debug(device, "Using random bytes to generate MAC"); + + /* We require genuine randomness here, since we want to make sure we won't collide with other + * systems booting up at the very same time. We do allow RDRAND however, since this is not + * cryptographic key material. */ + r = genuine_random_bytes(mac->ether_addr_octet, ETH_ALEN, RANDOM_ALLOW_RDRAND); + if (r < 0) + return log_device_error_errno(device, r, "Failed to acquire random data to generate MAC: %m"); + } else { + uint64_t result; + + r = net_get_unique_predictable_data(device, + naming_scheme_has(NAMING_STABLE_VIRTUAL_MACS), + &result); + if (r < 0) + return log_device_warning_errno(device, r, "Could not generate persistent MAC: %m"); + + log_device_debug(device, "Using generated persistent MAC address"); + assert_cc(ETH_ALEN <= sizeof(result)); + memcpy(mac->ether_addr_octet, &result, ETH_ALEN); + } + + /* see eth_random_addr in the kernel */ + mac->ether_addr_octet[0] &= 0xfe; /* clear multicast bit */ + mac->ether_addr_octet[0] |= 0x02; /* set local assignment bit (IEEE802) */ + return 1; +} + +static int link_config_apply_rtnl_settings(sd_netlink **rtnl, const link_config *config, sd_device *device) { + struct ether_addr generated_mac, *mac = NULL; + int ifindex, r; + + assert(rtnl); + assert(config); + assert(device); + + r = sd_device_get_ifindex(device, &ifindex); + if (r < 0) + return log_device_error_errno(device, r, "Could not find ifindex: %m"); + + if (IN_SET(config->mac_address_policy, MAC_ADDRESS_POLICY_PERSISTENT, MAC_ADDRESS_POLICY_RANDOM)) { + if (get_mac(device, config->mac_address_policy, &generated_mac) > 0) + mac = &generated_mac; + } else + mac = config->mac; + + r = rtnl_set_link_properties(rtnl, ifindex, config->alias, mac, config->mtu); + if (r < 0) + log_device_warning_errno(device, r, "Could not set Alias=, MACAddress= or MTU=, ignoring: %m"); + + return 0; +} + +static int link_config_generate_new_name(const link_config_ctx *ctx, const link_config *config, sd_device *device, const char **ret_name) { + unsigned name_type = NET_NAME_UNKNOWN; + const char *new_name = NULL; + NamePolicy policy; + int r; + + assert(ctx); + assert(config); + assert(device); + assert(ret_name); + + (void) link_unsigned_attribute(device, "name_assign_type", &name_type); + + if (IN_SET(name_type, NET_NAME_USER, NET_NAME_RENAMED) + && !naming_scheme_has(NAMING_ALLOW_RERENAMES)) { + log_device_debug(device, "Device already has a name given by userspace, not renaming."); + goto no_rename; + } + + if (ctx->enable_name_policy && config->name_policy) + for (NamePolicy *p = config->name_policy; *p != _NAMEPOLICY_INVALID; p++) { + policy = *p; + + switch (policy) { + case NAMEPOLICY_KERNEL: + if (name_type != NET_NAME_PREDICTABLE) + continue; + + /* The kernel claims to have given a predictable name, keep it. */ + log_device_debug(device, "Policy *%s*: keeping predictable kernel name", + name_policy_to_string(policy)); + goto no_rename; + case NAMEPOLICY_KEEP: + if (!IN_SET(name_type, NET_NAME_USER, NET_NAME_RENAMED)) + continue; + + log_device_debug(device, "Policy *%s*: keeping existing userspace name", + name_policy_to_string(policy)); + goto no_rename; + case NAMEPOLICY_DATABASE: + (void) sd_device_get_property_value(device, "ID_NET_NAME_FROM_DATABASE", &new_name); + break; + case NAMEPOLICY_ONBOARD: + (void) sd_device_get_property_value(device, "ID_NET_NAME_ONBOARD", &new_name); + break; + case NAMEPOLICY_SLOT: + (void) sd_device_get_property_value(device, "ID_NET_NAME_SLOT", &new_name); + break; + case NAMEPOLICY_PATH: + (void) sd_device_get_property_value(device, "ID_NET_NAME_PATH", &new_name); + break; + case NAMEPOLICY_MAC: + (void) sd_device_get_property_value(device, "ID_NET_NAME_MAC", &new_name); + break; + default: + assert_not_reached("invalid policy"); + } + if (ifname_valid(new_name)) + break; + } + + if (new_name) { + log_device_debug(device, "Policy *%s* yields \"%s\".", name_policy_to_string(policy), new_name); + *ret_name = new_name; + return 0; + } + + if (config->name) { + log_device_debug(device, "Policies didn't yield a name, using specified Name=%s.", config->name); + *ret_name = config->name; + return 0; + } + + log_device_debug(device, "Policies didn't yield a name and Name= is not given, not renaming."); +no_rename: + r = sd_device_get_sysname(device, ret_name); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get sysname: %m"); + + return 0; +} + +static int link_config_apply_alternative_names(sd_netlink **rtnl, const link_config *config, sd_device *device, const char *new_name) { + _cleanup_strv_free_ char **altnames = NULL, **current_altnames = NULL; + const char *current_name; + int ifindex, r; + + assert(rtnl); + assert(config); + assert(device); + + r = sd_device_get_sysname(device, ¤t_name); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get sysname: %m"); + + r = sd_device_get_ifindex(device, &ifindex); + if (r < 0) + return log_device_error_errno(device, r, "Could not find ifindex: %m"); + + if (config->alternative_names) { + altnames = strv_copy(config->alternative_names); + if (!altnames) + return log_oom(); + } + + if (config->alternative_names_policy) + for (NamePolicy *p = config->alternative_names_policy; *p != _NAMEPOLICY_INVALID; p++) { + const char *n = NULL; + + switch (*p) { + case NAMEPOLICY_DATABASE: + (void) sd_device_get_property_value(device, "ID_NET_NAME_FROM_DATABASE", &n); + break; + case NAMEPOLICY_ONBOARD: + (void) sd_device_get_property_value(device, "ID_NET_NAME_ONBOARD", &n); + break; + case NAMEPOLICY_SLOT: + (void) sd_device_get_property_value(device, "ID_NET_NAME_SLOT", &n); + break; + case NAMEPOLICY_PATH: + (void) sd_device_get_property_value(device, "ID_NET_NAME_PATH", &n); + break; + case NAMEPOLICY_MAC: + (void) sd_device_get_property_value(device, "ID_NET_NAME_MAC", &n); + break; + default: + assert_not_reached("invalid policy"); + } + if (!isempty(n)) { + r = strv_extend(&altnames, n); + if (r < 0) + return log_oom(); + } + } + + if (new_name) + strv_remove(altnames, new_name); + strv_remove(altnames, current_name); + + r = rtnl_get_link_alternative_names(rtnl, ifindex, ¤t_altnames); + if (r < 0) + log_device_debug_errno(device, r, "Failed to get alternative names, ignoring: %m"); + + char **p; + STRV_FOREACH(p, current_altnames) + strv_remove(altnames, *p); + + strv_uniq(altnames); + strv_sort(altnames); + r = rtnl_set_link_alternative_names(rtnl, ifindex, altnames); + if (r < 0) + log_device_full_errno(device, r == -EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, r, + "Could not set AlternativeName= or apply AlternativeNamesPolicy=, ignoring: %m"); + + return 0; +} + +int link_config_apply(link_config_ctx *ctx, const link_config *config, sd_device *device, const char **ret_name) { + const char *new_name; + DeviceAction a; + int r; + + assert(ctx); + assert(config); + assert(device); + assert(ret_name); + + r = device_get_action(device, &a); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get ACTION= property: %m"); + + if (!IN_SET(a, DEVICE_ACTION_ADD, DEVICE_ACTION_BIND, DEVICE_ACTION_MOVE)) { + log_device_debug(device, "Skipping to apply .link settings on '%s' uevent.", device_action_to_string(a)); + + r = sd_device_get_sysname(device, ret_name); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get sysname: %m"); + + return 0; + } + + r = link_config_apply_ethtool_settings(&ctx->ethtool_fd, config, device); + if (r < 0) + return r; + + r = link_config_apply_rtnl_settings(&ctx->rtnl, config, device); + if (r < 0) + return r; + + if (a == DEVICE_ACTION_MOVE) { + log_device_debug(device, "Skipping to apply Name= and NamePolicy= on '%s' uevent.", device_action_to_string(a)); + + r = sd_device_get_sysname(device, &new_name); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get sysname: %m"); + } else { + r = link_config_generate_new_name(ctx, config, device, &new_name); + if (r < 0) + return r; + } + + r = link_config_apply_alternative_names(&ctx->rtnl, config, device, new_name); + if (r < 0) + return r; + + *ret_name = new_name; + return 0; +} + +int link_get_driver(link_config_ctx *ctx, sd_device *device, char **ret) { + const char *name; + char *driver = NULL; + int r; + + r = sd_device_get_sysname(device, &name); + if (r < 0) + return r; + + r = ethtool_get_driver(&ctx->ethtool_fd, name, &driver); + if (r < 0) + return r; + + *ret = driver; + return 0; +} + +static const char* const mac_address_policy_table[_MAC_ADDRESS_POLICY_MAX] = { + [MAC_ADDRESS_POLICY_PERSISTENT] = "persistent", + [MAC_ADDRESS_POLICY_RANDOM] = "random", + [MAC_ADDRESS_POLICY_NONE] = "none", +}; + +DEFINE_STRING_TABLE_LOOKUP(mac_address_policy, MACAddressPolicy); +DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT( + config_parse_mac_address_policy, + mac_address_policy, + MACAddressPolicy, + MAC_ADDRESS_POLICY_NONE, + "Failed to parse MAC address policy"); + +static const char* const name_policy_table[_NAMEPOLICY_MAX] = { + [NAMEPOLICY_KERNEL] = "kernel", + [NAMEPOLICY_KEEP] = "keep", + [NAMEPOLICY_DATABASE] = "database", + [NAMEPOLICY_ONBOARD] = "onboard", + [NAMEPOLICY_SLOT] = "slot", + [NAMEPOLICY_PATH] = "path", + [NAMEPOLICY_MAC] = "mac", +}; + +DEFINE_STRING_TABLE_LOOKUP(name_policy, NamePolicy); +DEFINE_CONFIG_PARSE_ENUMV(config_parse_name_policy, name_policy, NamePolicy, + _NAMEPOLICY_INVALID, + "Failed to parse interface name policy"); + +static const char* const alternative_names_policy_table[_NAMEPOLICY_MAX] = { + [NAMEPOLICY_DATABASE] = "database", + [NAMEPOLICY_ONBOARD] = "onboard", + [NAMEPOLICY_SLOT] = "slot", + [NAMEPOLICY_PATH] = "path", + [NAMEPOLICY_MAC] = "mac", +}; + +DEFINE_STRING_TABLE_LOOKUP(alternative_names_policy, NamePolicy); +DEFINE_CONFIG_PARSE_ENUMV(config_parse_alternative_names_policy, alternative_names_policy, NamePolicy, + _NAMEPOLICY_INVALID, + "Failed to parse alternative names policy"); diff --git a/src/udev/net/link-config.h b/src/udev/net/link-config.h new file mode 100644 index 0000000..eab1849 --- /dev/null +++ b/src/udev/net/link-config.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-device.h" + +#include "condition.h" +#include "conf-parser.h" +#include "ethtool-util.h" +#include "list.h" +#include "set.h" + +typedef struct link_config_ctx link_config_ctx; +typedef struct link_config link_config; + +typedef enum MACAddressPolicy { + MAC_ADDRESS_POLICY_PERSISTENT, + MAC_ADDRESS_POLICY_RANDOM, + MAC_ADDRESS_POLICY_NONE, + _MAC_ADDRESS_POLICY_MAX, + _MAC_ADDRESS_POLICY_INVALID = -1 +} MACAddressPolicy; + +typedef enum NamePolicy { + NAMEPOLICY_KERNEL, + NAMEPOLICY_KEEP, + NAMEPOLICY_DATABASE, + NAMEPOLICY_ONBOARD, + NAMEPOLICY_SLOT, + NAMEPOLICY_PATH, + NAMEPOLICY_MAC, + _NAMEPOLICY_MAX, + _NAMEPOLICY_INVALID = -1 +} NamePolicy; + +struct link_config { + char *filename; + + Set *match_mac; + Set *match_permanent_mac; + char **match_path; + char **match_driver; + char **match_type; + char **match_name; + char **match_property; + LIST_HEAD(Condition, conditions); + + char *description; + struct ether_addr *mac; + MACAddressPolicy mac_address_policy; + NamePolicy *name_policy; + NamePolicy *alternative_names_policy; + char *name; + char **alternative_names; + char *alias; + uint32_t mtu; + uint64_t speed; + Duplex duplex; + int autonegotiation; + uint32_t advertise[N_ADVERTISE]; + WakeOnLan wol; + NetDevPort port; + int features[_NET_DEV_FEAT_MAX]; + netdev_channels channels; + netdev_ring_param ring; + int rx_flow_control; + int tx_flow_control; + int autoneg_flow_control; + + LIST_FIELDS(link_config, links); +}; + +int link_config_ctx_new(link_config_ctx **ret); +void link_config_ctx_free(link_config_ctx *ctx); +DEFINE_TRIVIAL_CLEANUP_FUNC(link_config_ctx*, link_config_ctx_free); + +int link_load_one(link_config_ctx *ctx, const char *filename); +int link_config_load(link_config_ctx *ctx); +bool link_config_should_reload(link_config_ctx *ctx); + +int link_config_get(link_config_ctx *ctx, sd_device *device, link_config **ret); +int link_config_apply(link_config_ctx *ctx, const link_config *config, sd_device *device, const char **ret_name); +int link_get_driver(link_config_ctx *ctx, sd_device *device, char **ret); + +const char *name_policy_to_string(NamePolicy p) _const_; +NamePolicy name_policy_from_string(const char *p) _pure_; + +const char *alternative_names_policy_to_string(NamePolicy p) _const_; +NamePolicy alternative_names_policy_from_string(const char *p) _pure_; + +const char *mac_address_policy_to_string(MACAddressPolicy p) _const_; +MACAddressPolicy mac_address_policy_from_string(const char *p) _pure_; + +/* gperf lookup function */ +const struct ConfigPerfItem* link_config_gperf_lookup(const char *key, GPERF_LEN_TYPE length); + +CONFIG_PARSER_PROTOTYPE(config_parse_mac_address_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_name_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_alternative_names_policy); diff --git a/src/udev/scsi_id/README b/src/udev/scsi_id/README new file mode 100644 index 0000000..9cfe739 --- /dev/null +++ b/src/udev/scsi_id/README @@ -0,0 +1,4 @@ +scsi_id - generate a SCSI unique identifier for a given SCSI device + +Please send questions, comments or patches to <patmans@us.ibm.com> or +<linux-hotplug-devel@lists.sourceforge.net>. diff --git a/src/udev/scsi_id/scsi.h b/src/udev/scsi_id/scsi.h new file mode 100644 index 0000000..ee3e401 --- /dev/null +++ b/src/udev/scsi_id/scsi.h @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * scsi.h + * + * General scsi and linux scsi specific defines and structs. + * + * Copyright (C) IBM Corp. 2003 + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation version 2 of the License. + */ + +#include <scsi/scsi.h> + +struct scsi_ioctl_command { + unsigned inlen; /* excluding scsi command length */ + unsigned outlen; + unsigned char data[1]; + /* on input, scsi command starts here then opt. data */ +}; + +/* + * Default 5 second timeout + */ +#define DEF_TIMEOUT 5000 + +#define SENSE_BUFF_LEN 32 + +/* + * The request buffer size passed to the SCSI INQUIRY commands, use 254, + * as this is a nice value for some devices, especially some of the usb + * mass storage devices. + */ +#define SCSI_INQ_BUFF_LEN 254 + +/* + * SCSI INQUIRY vendor and model (really product) lengths. + */ +#define VENDOR_LENGTH 8 +#define MODEL_LENGTH 16 + +#define INQUIRY_CMD 0x12 +#define INQUIRY_CMDLEN 6 + +/* + * INQUIRY VPD page 0x83 identifier descriptor related values. Reference the + * SCSI Primary Commands specification for details. + */ + +/* + * id type values of id descriptors. These are assumed to fit in 4 bits. + */ +#define SCSI_ID_VENDOR_SPECIFIC 0 +#define SCSI_ID_T10_VENDOR 1 +#define SCSI_ID_EUI_64 2 +#define SCSI_ID_NAA 3 +#define SCSI_ID_RELPORT 4 +#define SCSI_ID_TGTGROUP 5 +#define SCSI_ID_LUNGROUP 6 +#define SCSI_ID_MD5 7 +#define SCSI_ID_NAME 8 + +/* + * Supported NAA values. These fit in 4 bits, so the "don't care" value + * cannot conflict with real values. + */ +#define SCSI_ID_NAA_DONT_CARE 0xff +#define SCSI_ID_NAA_IEEE_REG 0x05 +#define SCSI_ID_NAA_IEEE_REG_EXTENDED 0x06 + +/* + * Supported Code Set values. + */ +#define SCSI_ID_BINARY 1 +#define SCSI_ID_ASCII 2 + +struct scsi_id_search_values { + u_char id_type; + u_char naa_type; + u_char code_set; +}; + +/* + * Following are the "true" SCSI status codes. Linux has traditionally + * used a 1 bit right and masked version of these. So now CHECK_CONDITION + * and friends (in <scsi/scsi.h>) are deprecated. + */ +#define SCSI_CHECK_CONDITION 0x02 +#define SCSI_CONDITION_MET 0x04 +#define SCSI_BUSY 0x08 +#define SCSI_IMMEDIATE 0x10 +#define SCSI_IMMEDIATE_CONDITION_MET 0x14 +#define SCSI_RESERVATION_CONFLICT 0x18 +#define SCSI_COMMAND_TERMINATED 0x22 +#define SCSI_TASK_SET_FULL 0x28 +#define SCSI_ACA_ACTIVE 0x30 +#define SCSI_TASK_ABORTED 0x40 diff --git a/src/udev/scsi_id/scsi_id.c b/src/udev/scsi_id/scsi_id.c new file mode 100644 index 0000000..5720256 --- /dev/null +++ b/src/udev/scsi_id/scsi_id.c @@ -0,0 +1,595 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © IBM Corp. 2003 + * Copyright © SUSE Linux Products GmbH, 2006 + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "build.h" +#include "fd-util.h" +#include "libudev-util.h" +#include "scsi_id.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-util.h" + +static const struct option options[] = { + { "device", required_argument, NULL, 'd' }, + { "config", required_argument, NULL, 'f' }, + { "page", required_argument, NULL, 'p' }, + { "blacklisted", no_argument, NULL, 'b' }, + { "whitelisted", no_argument, NULL, 'g' }, + { "replace-whitespace", no_argument, NULL, 'u' }, + { "sg-version", required_argument, NULL, 's' }, + { "verbose", no_argument, NULL, 'v' }, + { "version", no_argument, NULL, 'V' }, /* don't advertise -V */ + { "export", no_argument, NULL, 'x' }, + { "help", no_argument, NULL, 'h' }, + {} +}; + +static bool all_good = false; +static bool dev_specified = false; +static char config_file[MAX_PATH_LEN] = "/etc/scsi_id.config"; +static enum page_code default_page_code = PAGE_UNSPECIFIED; +static int sg_version = 4; +static bool reformat_serial = false; +static bool export = false; +static char vendor_str[64]; +static char model_str[64]; +static char vendor_enc_str[256]; +static char model_enc_str[256]; +static char revision_str[16]; +static char type_str[16]; + +static void set_type(const char *from, char *to, size_t len) { + int type_num; + char *eptr; + const char *type = "generic"; + + type_num = strtoul(from, &eptr, 0); + if (eptr != from) { + switch (type_num) { + case 0: + type = "disk"; + break; + case 1: + type = "tape"; + break; + case 4: + type = "optical"; + break; + case 5: + type = "cd"; + break; + case 7: + type = "optical"; + break; + case 0xe: + type = "disk"; + break; + case 0xf: + type = "optical"; + break; + default: + break; + } + } + strscpy(to, len, type); +} + +/* + * get_value: + * + * buf points to an '=' followed by a quoted string ("foo") or a string ending + * with a space or ','. + * + * Return a pointer to the NUL terminated string, returns NULL if no + * matches. + */ +static char *get_value(char **buffer) { + static const char *quote_string = "\"\n"; + static const char *comma_string = ",\n"; + char *val; + const char *end; + + if (**buffer == '"') { + /* + * skip leading quote, terminate when quote seen + */ + (*buffer)++; + end = quote_string; + } else + end = comma_string; + val = strsep(buffer, end); + if (val && end == quote_string) + /* + * skip trailing quote + */ + (*buffer)++; + + while (isspace(**buffer)) + (*buffer)++; + + return val; +} + +static int argc_count(char *opts) { + int i = 0; + while (*opts != '\0') + if (*opts++ == ' ') + i++; + return i; +} + +/* + * get_file_options: + * + * If vendor == NULL, find a line in the config file with only "OPTIONS="; + * if vendor and model are set find the first OPTIONS line in the config + * file that matches. Set argc and argv to match the OPTIONS string. + * + * vendor and model can end in '\n'. + */ +static int get_file_options(const char *vendor, const char *model, + int *argc, char ***newargv) { + _cleanup_free_ char *buffer = NULL; + _cleanup_fclose_ FILE *f; + char *buf; + char *str1; + char *vendor_in, *model_in, *options_in; /* read in from file */ + int lineno; + int c; + int retval = 0; + + f = fopen(config_file, "re"); + if (!f) { + if (errno == ENOENT) + return 1; + else { + log_error_errno(errno, "can't open %s: %m", config_file); + return -1; + } + } + + /* + * Allocate a buffer rather than put it on the stack so we can + * keep it around to parse any options (any allocated newargv + * points into this buffer for its strings). + */ + buffer = malloc(MAX_BUFFER_LEN); + if (!buffer) + return log_oom(); + + *newargv = NULL; + lineno = 0; + for (;;) { + vendor_in = model_in = options_in = NULL; + + buf = fgets(buffer, MAX_BUFFER_LEN, f); + if (!buf) + break; + lineno++; + if (buf[strlen(buffer) - 1] != '\n') { + log_error("Config file line %d too long", lineno); + break; + } + + while (isspace(*buf)) + buf++; + + /* blank or all whitespace line */ + if (*buf == '\0') + continue; + + /* comment line */ + if (*buf == '#') + continue; + + str1 = strsep(&buf, "="); + if (str1 && strcaseeq(str1, "VENDOR")) { + str1 = get_value(&buf); + if (!str1) { + retval = log_oom(); + break; + } + vendor_in = str1; + + str1 = strsep(&buf, "="); + if (str1 && strcaseeq(str1, "MODEL")) { + str1 = get_value(&buf); + if (!str1) { + retval = log_oom(); + break; + } + model_in = str1; + str1 = strsep(&buf, "="); + } + } + + if (str1 && strcaseeq(str1, "OPTIONS")) { + str1 = get_value(&buf); + if (!str1) { + retval = log_oom(); + break; + } + options_in = str1; + } + + /* + * Only allow: [vendor=foo[,model=bar]]options=stuff + */ + if (!options_in || (!vendor_in && model_in)) { + log_error("Error parsing config file line %d '%s'", lineno, buffer); + retval = -1; + break; + } + if (!vendor) { + if (!vendor_in) + break; + } else if (vendor_in && + startswith(vendor, vendor_in) && + (!model_in || startswith(model, model_in))) { + /* + * Matched vendor and optionally model. + * + * Note: a short vendor_in or model_in can + * give a partial match (that is FOO + * matches FOOBAR). + */ + break; + } + } + + if (retval == 0) { + if (vendor_in != NULL || model_in != NULL || + options_in != NULL) { + /* + * Something matched. Allocate newargv, and store + * values found in options_in. + */ + strcpy(buffer, options_in); + c = argc_count(buffer) + 2; + *newargv = calloc(c, sizeof(**newargv)); + if (!*newargv) + retval = log_oom(); + else { + *argc = c; + c = 0; + /* + * argv[0] at 0 is skipped by getopt, but + * store the buffer address there for + * later freeing + */ + (*newargv)[c] = buffer; + for (c = 1; c < *argc; c++) + (*newargv)[c] = strsep(&buffer, " \t"); + buffer = NULL; + } + } else { + /* No matches */ + retval = 1; + } + } + return retval; +} + +static void help(void) { + printf("Usage: %s [OPTION...] DEVICE\n\n" + "SCSI device identification.\n\n" + " -h --help Print this message\n" + " --version Print version of the program\n\n" + " -d --device= Device node for SG_IO commands\n" + " -f --config= Location of config file\n" + " -p --page=0x80|0x83|pre-spc3-83 SCSI page (0x80, 0x83, pre-spc3-83)\n" + " -s --sg-version=3|4 Use SGv3 or SGv4\n" + " -b --blacklisted Treat device as blacklisted\n" + " -g --whitelisted Treat device as whitelisted\n" + " -u --replace-whitespace Replace all whitespace by underscores\n" + " -v --verbose Verbose logging\n" + " -x --export Print values as environment keys\n" + , program_invocation_short_name); + +} + +static int set_options(int argc, char **argv, + char *maj_min_dev) { + int option; + + /* + * optind is a global extern used by getopt. Since we can call + * set_options twice (once for command line, and once for config + * file) we have to reset this back to 1. + */ + optind = 1; + while ((option = getopt_long(argc, argv, "d:f:gp:uvVxhbs:", options, NULL)) >= 0) + switch (option) { + case 'b': + all_good = false; + break; + + case 'd': + dev_specified = true; + strscpy(maj_min_dev, MAX_PATH_LEN, optarg); + break; + + case 'f': + strscpy(config_file, MAX_PATH_LEN, optarg); + break; + + case 'g': + all_good = true; + break; + + case 'h': + help(); + exit(EXIT_SUCCESS); + + case 'p': + if (streq(optarg, "0x80")) + default_page_code = PAGE_80; + else if (streq(optarg, "0x83")) + default_page_code = PAGE_83; + else if (streq(optarg, "pre-spc3-83")) + default_page_code = PAGE_83_PRE_SPC3; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown page code '%s'", + optarg); + break; + + case 's': + sg_version = atoi(optarg); + if (sg_version < 3 || sg_version > 4) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown SG version '%s'", + optarg); + break; + + case 'u': + reformat_serial = true; + break; + + case 'v': + log_set_target(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_DEBUG); + log_open(); + break; + + case 'V': + printf("%s\n", GIT_VERSION); + exit(EXIT_SUCCESS); + + case 'x': + export = true; + break; + + case '?': + return -1; + + default: + assert_not_reached("Unknown option"); + } + + if (optind < argc && !dev_specified) { + dev_specified = true; + strscpy(maj_min_dev, MAX_PATH_LEN, argv[optind]); + } + + return 0; +} + +static int per_dev_options(struct scsi_id_device *dev_scsi, int *good_bad, int *page_code) { + int retval; + int newargc; + char **newargv = NULL; + int option; + + *good_bad = all_good; + *page_code = default_page_code; + + retval = get_file_options(vendor_str, model_str, &newargc, &newargv); + + optind = 1; /* reset this global extern */ + while (retval == 0) { + option = getopt_long(newargc, newargv, "bgp:", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'b': + *good_bad = 0; + break; + + case 'g': + *good_bad = 1; + break; + + case 'p': + if (streq(optarg, "0x80")) { + *page_code = PAGE_80; + } else if (streq(optarg, "0x83")) { + *page_code = PAGE_83; + } else if (streq(optarg, "pre-spc3-83")) { + *page_code = PAGE_83_PRE_SPC3; + } else { + log_error("Unknown page code '%s'", optarg); + retval = -1; + } + break; + + default: + log_error("Unknown or bad option '%c' (0x%x)", option, option); + retval = -1; + break; + } + } + + if (newargv) { + free(newargv[0]); + free(newargv); + } + return retval; +} + +static int set_inq_values(struct scsi_id_device *dev_scsi, const char *path) { + int retval; + + dev_scsi->use_sg = sg_version; + + retval = scsi_std_inquiry(dev_scsi, path); + if (retval) + return retval; + + udev_util_encode_string(dev_scsi->vendor, vendor_enc_str, sizeof(vendor_enc_str)); + udev_util_encode_string(dev_scsi->model, model_enc_str, sizeof(model_enc_str)); + + util_replace_whitespace(dev_scsi->vendor, vendor_str, sizeof(vendor_str)-1); + util_replace_chars(vendor_str, NULL); + util_replace_whitespace(dev_scsi->model, model_str, sizeof(model_str)-1); + util_replace_chars(model_str, NULL); + set_type(dev_scsi->type, type_str, sizeof(type_str)); + util_replace_whitespace(dev_scsi->revision, revision_str, sizeof(revision_str)-1); + util_replace_chars(revision_str, NULL); + return 0; +} + +/* + * scsi_id: try to get an id, if one is found, printf it to stdout. + * returns a value passed to exit() - 0 if printed an id, else 1. + */ +static int scsi_id(char *maj_min_dev) { + struct scsi_id_device dev_scsi = {}; + int good_dev; + int page_code; + int retval = 0; + + if (set_inq_values(&dev_scsi, maj_min_dev) < 0) { + retval = 1; + goto out; + } + + /* get per device (vendor + model) options from the config file */ + per_dev_options(&dev_scsi, &good_dev, &page_code); + if (!good_dev) { + retval = 1; + goto out; + } + + /* read serial number from mode pages (no values for optical drives) */ + scsi_get_serial(&dev_scsi, maj_min_dev, page_code, MAX_SERIAL_LEN); + + if (export) { + char serial_str[MAX_SERIAL_LEN]; + + printf("ID_SCSI=1\n"); + printf("ID_VENDOR=%s\n", vendor_str); + printf("ID_VENDOR_ENC=%s\n", vendor_enc_str); + printf("ID_MODEL=%s\n", model_str); + printf("ID_MODEL_ENC=%s\n", model_enc_str); + printf("ID_REVISION=%s\n", revision_str); + printf("ID_TYPE=%s\n", type_str); + if (dev_scsi.serial[0] != '\0') { + util_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1); + util_replace_chars(serial_str, NULL); + printf("ID_SERIAL=%s\n", serial_str); + util_replace_whitespace(dev_scsi.serial_short, serial_str, sizeof(serial_str)-1); + util_replace_chars(serial_str, NULL); + printf("ID_SERIAL_SHORT=%s\n", serial_str); + } + if (dev_scsi.wwn[0] != '\0') { + printf("ID_WWN=0x%s\n", dev_scsi.wwn); + if (dev_scsi.wwn_vendor_extension[0] != '\0') { + printf("ID_WWN_VENDOR_EXTENSION=0x%s\n", dev_scsi.wwn_vendor_extension); + printf("ID_WWN_WITH_EXTENSION=0x%s%s\n", dev_scsi.wwn, dev_scsi.wwn_vendor_extension); + } else + printf("ID_WWN_WITH_EXTENSION=0x%s\n", dev_scsi.wwn); + } + if (dev_scsi.tgpt_group[0] != '\0') + printf("ID_TARGET_PORT=%s\n", dev_scsi.tgpt_group); + if (dev_scsi.unit_serial_number[0] != '\0') + printf("ID_SCSI_SERIAL=%s\n", dev_scsi.unit_serial_number); + goto out; + } + + if (dev_scsi.serial[0] == '\0') { + retval = 1; + goto out; + } + + if (reformat_serial) { + char serial_str[MAX_SERIAL_LEN]; + + util_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1); + util_replace_chars(serial_str, NULL); + printf("%s\n", serial_str); + goto out; + } + + printf("%s\n", dev_scsi.serial); +out: + return retval; +} + +int main(int argc, char **argv) { + int retval = 0; + char maj_min_dev[MAX_PATH_LEN]; + int newargc; + char **newargv = NULL; + + log_set_target(LOG_TARGET_AUTO); + udev_parse_config(); + log_parse_environment(); + log_open(); + + /* + * Get config file options. + */ + retval = get_file_options(NULL, NULL, &newargc, &newargv); + if (retval < 0) { + retval = 1; + goto exit; + } + if (retval == 0) { + assert(newargv); + + if (set_options(newargc, newargv, maj_min_dev) < 0) { + retval = 2; + goto exit; + } + } + + /* + * Get command line options (overriding any config file settings). + */ + if (set_options(argc, argv, maj_min_dev) < 0) + exit(EXIT_FAILURE); + + if (!dev_specified) { + log_error("No device specified."); + retval = 1; + goto exit; + } + + retval = scsi_id(maj_min_dev); + +exit: + if (newargv) { + free(newargv[0]); + free(newargv); + } + log_close(); + return retval; +} diff --git a/src/udev/scsi_id/scsi_id.h b/src/udev/scsi_id/scsi_id.h new file mode 100644 index 0000000..2fe64f4 --- /dev/null +++ b/src/udev/scsi_id/scsi_id.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * Copyright © IBM Corp. 2003 + */ + +#define MAX_PATH_LEN 512 + +/* + * MAX_ATTR_LEN: maximum length of the result of reading a sysfs + * attribute. + */ +#define MAX_ATTR_LEN 256 + +/* + * MAX_SERIAL_LEN: the maximum length of the serial number, including + * added prefixes such as vendor and product (model) strings. + */ +#define MAX_SERIAL_LEN 256 + +/* + * MAX_BUFFER_LEN: maximum buffer size and line length used while reading + * the config file. + */ +#define MAX_BUFFER_LEN 256 + +struct scsi_id_device { + char vendor[9]; + char model[17]; + char revision[5]; + char type[33]; + char kernel[64]; + char serial[MAX_SERIAL_LEN]; + char serial_short[MAX_SERIAL_LEN]; + int use_sg; + + /* Always from page 0x80 e.g. 'B3G1P8500RWT' - may not be unique */ + char unit_serial_number[MAX_SERIAL_LEN]; + + /* NULs if not set - otherwise hex encoding using lower-case e.g. '50014ee0016eb572' */ + char wwn[17]; + + /* NULs if not set - otherwise hex encoding using lower-case e.g. '0xe00000d80000' */ + char wwn_vendor_extension[17]; + + /* NULs if not set - otherwise decimal number */ + char tgpt_group[8]; +}; + +int scsi_std_inquiry(struct scsi_id_device *dev_scsi, const char *devname); +int scsi_get_serial(struct scsi_id_device *dev_scsi, const char *devname, + int page_code, int len); + +/* + * Page code values. + */ +enum page_code { + PAGE_83_PRE_SPC3 = -0x83, + PAGE_UNSPECIFIED = 0x00, + PAGE_80 = 0x80, + PAGE_83 = 0x83, +}; diff --git a/src/udev/scsi_id/scsi_serial.c b/src/udev/scsi_id/scsi_serial.c new file mode 100644 index 0000000..4fe7254 --- /dev/null +++ b/src/udev/scsi_id/scsi_serial.c @@ -0,0 +1,893 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © IBM Corp. 2003 + * + * Author: Patrick Mansfield<patmans@us.ibm.com> + */ + +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <linux/bsg.h> +#include <linux/types.h> +#include <scsi/scsi.h> +#include <scsi/sg.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include "memory-util.h" +#include "random-util.h" +#include "scsi.h" +#include "scsi_id.h" +#include "string-util.h" + +/* + * A priority based list of id, naa, and binary/ascii for the identifier + * descriptor in VPD page 0x83. + * + * Brute force search for a match starting with the first value in the + * following id_search_list. This is not a performance issue, since there + * is normally one or some small number of descriptors. + */ +static const struct scsi_id_search_values id_search_list[] = { + { SCSI_ID_TGTGROUP, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG_EXTENDED, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG_EXTENDED, SCSI_ID_ASCII }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG, SCSI_ID_ASCII }, + /* + * Devices already exist using NAA values that are now marked + * reserved. These should not conflict with other values, or it is + * a bug in the device. As long as we find the IEEE extended one + * first, we really don't care what other ones are used. Using + * don't care here means that a device that returns multiple + * non-IEEE descriptors in a random order will get different + * names. + */ + { SCSI_ID_NAA, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_NAA, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, + { SCSI_ID_EUI_64, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_EUI_64, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, + { SCSI_ID_T10_VENDOR, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_T10_VENDOR, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, + { SCSI_ID_VENDOR_SPECIFIC, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY }, + { SCSI_ID_VENDOR_SPECIFIC, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII }, +}; + +static const char hex_str[]="0123456789abcdef"; + +/* + * Values returned in the result/status, only the ones used by the code + * are used here. + */ + +#define DID_NO_CONNECT 0x01 /* Unable to connect before timeout */ +#define DID_BUS_BUSY 0x02 /* Bus remain busy until timeout */ +#define DID_TIME_OUT 0x03 /* Timed out for some other reason */ +#define DRIVER_TIMEOUT 0x06 +#define DRIVER_SENSE 0x08 /* Sense_buffer has been set */ + +/* The following "category" function returns one of the following */ +#define SG_ERR_CAT_CLEAN 0 /* No errors or other information */ +#define SG_ERR_CAT_MEDIA_CHANGED 1 /* interpreted from sense buffer */ +#define SG_ERR_CAT_RESET 2 /* interpreted from sense buffer */ +#define SG_ERR_CAT_TIMEOUT 3 +#define SG_ERR_CAT_RECOVERED 4 /* Successful command after recovered err */ +#define SG_ERR_CAT_NOTSUPPORTED 5 /* Illegal / unsupported command */ +#define SG_ERR_CAT_SENSE 98 /* Something else in the sense buffer */ +#define SG_ERR_CAT_OTHER 99 /* Some other error/warning */ + +static int do_scsi_page80_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int max_len); + +static int sg_err_category_new(int scsi_status, int msg_status, int + host_status, int driver_status, const + unsigned char *sense_buffer, int sb_len) { + scsi_status &= 0x7e; + + /* + * XXX change to return only two values - failed or OK. + */ + + if (!scsi_status && !host_status && !driver_status) + return SG_ERR_CAT_CLEAN; + + if (IN_SET(scsi_status, SCSI_CHECK_CONDITION, SCSI_COMMAND_TERMINATED) || + (driver_status & 0xf) == DRIVER_SENSE) { + if (sense_buffer && (sb_len > 2)) { + int sense_key; + unsigned char asc; + + if (sense_buffer[0] & 0x2) { + sense_key = sense_buffer[1] & 0xf; + asc = sense_buffer[2]; + } else { + sense_key = sense_buffer[2] & 0xf; + asc = (sb_len > 12) ? sense_buffer[12] : 0; + } + + if (sense_key == RECOVERED_ERROR) + return SG_ERR_CAT_RECOVERED; + else if (sense_key == UNIT_ATTENTION) { + if (0x28 == asc) + return SG_ERR_CAT_MEDIA_CHANGED; + if (0x29 == asc) + return SG_ERR_CAT_RESET; + } else if (sense_key == ILLEGAL_REQUEST) + return SG_ERR_CAT_NOTSUPPORTED; + } + return SG_ERR_CAT_SENSE; + } + if (host_status) { + if (IN_SET(host_status, DID_NO_CONNECT, DID_BUS_BUSY, DID_TIME_OUT)) + return SG_ERR_CAT_TIMEOUT; + } + if (driver_status) { + if (driver_status == DRIVER_TIMEOUT) + return SG_ERR_CAT_TIMEOUT; + } + return SG_ERR_CAT_OTHER; +} + +static int sg_err_category3(struct sg_io_hdr *hp) { + return sg_err_category_new(hp->status, hp->msg_status, + hp->host_status, hp->driver_status, + hp->sbp, hp->sb_len_wr); +} + +static int sg_err_category4(struct sg_io_v4 *hp) { + return sg_err_category_new(hp->device_status, 0, + hp->transport_status, hp->driver_status, + (unsigned char *)(uintptr_t)hp->response, + hp->response_len); +} + +static int scsi_dump_sense(struct scsi_id_device *dev_scsi, + unsigned char *sense_buffer, int sb_len) { + int s; + int code; + int sense_class; + int sense_key; + int asc, ascq; + + /* + * Figure out and print the sense key, asc and ascq. + * + * If you want to suppress these for a particular drive model, add + * a black list entry in the scsi_id config file. + * + * XXX We probably need to: lookup the sense/asc/ascq in a retry + * table, and if found return 1 (after dumping the sense, asc, and + * ascq). So, if/when we get something like a power on/reset, + * we'll retry the command. + */ + + if (sb_len < 1) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense buffer empty", + dev_scsi->kernel); + + sense_class = (sense_buffer[0] >> 4) & 0x07; + code = sense_buffer[0] & 0xf; + + if (sense_class == 7) { + /* + * extended sense data. + */ + s = sense_buffer[7] + 8; + if (sb_len < s) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense buffer too small %d bytes, %d bytes too short", + dev_scsi->kernel, sb_len, + s - sb_len); + + if (IN_SET(code, 0x0, 0x1)) { + sense_key = sense_buffer[2] & 0xf; + if (s < 14) + /* + * Possible? + */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense result too small %d bytes", + dev_scsi->kernel, s); + + asc = sense_buffer[12]; + ascq = sense_buffer[13]; + } else if (IN_SET(code, 0x2, 0x3)) { + sense_key = sense_buffer[1] & 0xf; + asc = sense_buffer[2]; + ascq = sense_buffer[3]; + } else + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: invalid sense code 0x%x", + dev_scsi->kernel, code); + + log_debug("%s: sense key 0x%x ASC 0x%x ASCQ 0x%x", + dev_scsi->kernel, sense_key, asc, ascq); + } else { + if (sb_len < 4) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: sense buffer too small %d bytes, %d bytes too short", + dev_scsi->kernel, sb_len, + 4 - sb_len); + + if (sense_buffer[0] < 15) + log_debug("%s: old sense key: 0x%x", dev_scsi->kernel, sense_buffer[0] & 0x0f); + else + log_debug("%s: sense = %2x %2x", + dev_scsi->kernel, sense_buffer[0], sense_buffer[2]); + log_debug("%s: non-extended sense class %d code 0x%0x", + dev_scsi->kernel, sense_class, code); + + } + + return -1; +} + +static int scsi_dump(struct scsi_id_device *dev_scsi, struct sg_io_hdr *io) { + if (!io->status && !io->host_status && !io->msg_status && + !io->driver_status) + /* + * Impossible, should not be called. + */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: called with no error", + __FUNCTION__); + + log_debug("%s: sg_io failed status 0x%x 0x%x 0x%x 0x%x", + dev_scsi->kernel, io->driver_status, io->host_status, io->msg_status, io->status); + if (io->status == SCSI_CHECK_CONDITION) + return scsi_dump_sense(dev_scsi, io->sbp, io->sb_len_wr); + else + return -1; +} + +static int scsi_dump_v4(struct scsi_id_device *dev_scsi, struct sg_io_v4 *io) { + if (!io->device_status && !io->transport_status && + !io->driver_status) + /* + * Impossible, should not be called. + */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: called with no error", + __FUNCTION__); + + log_debug("%s: sg_io failed status 0x%x 0x%x 0x%x", + dev_scsi->kernel, io->driver_status, io->transport_status, io->device_status); + if (io->device_status == SCSI_CHECK_CONDITION) + return scsi_dump_sense(dev_scsi, (unsigned char *)(uintptr_t)io->response, + io->response_len); + else + return -1; +} + +static int scsi_inquiry(struct scsi_id_device *dev_scsi, int fd, + unsigned char evpd, unsigned char page, + unsigned char *buf, unsigned buflen) { + unsigned char inq_cmd[INQUIRY_CMDLEN] = + { INQUIRY_CMD, evpd, page, 0, buflen, 0 }; + unsigned char sense[SENSE_BUFF_LEN]; + void *io_buf; + struct sg_io_v4 io_v4; + struct sg_io_hdr io_hdr; + int retry = 3; /* rather random */ + int retval; + + if (buflen > SCSI_INQ_BUFF_LEN) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "buflen %d too long", buflen); + +resend: + if (dev_scsi->use_sg == 4) { + memzero(&io_v4, sizeof(struct sg_io_v4)); + io_v4.guard = 'Q'; + io_v4.protocol = BSG_PROTOCOL_SCSI; + io_v4.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD; + io_v4.request_len = sizeof(inq_cmd); + io_v4.request = (uintptr_t)inq_cmd; + io_v4.max_response_len = sizeof(sense); + io_v4.response = (uintptr_t)sense; + io_v4.din_xfer_len = buflen; + io_v4.din_xferp = (uintptr_t)buf; + io_buf = (void *)&io_v4; + } else { + memzero(&io_hdr, sizeof(struct sg_io_hdr)); + io_hdr.interface_id = 'S'; + io_hdr.cmd_len = sizeof(inq_cmd); + io_hdr.mx_sb_len = sizeof(sense); + io_hdr.dxfer_direction = SG_DXFER_FROM_DEV; + io_hdr.dxfer_len = buflen; + io_hdr.dxferp = buf; + io_hdr.cmdp = inq_cmd; + io_hdr.sbp = sense; + io_hdr.timeout = DEF_TIMEOUT; + io_buf = (void *)&io_hdr; + } + + retval = ioctl(fd, SG_IO, io_buf); + if (retval < 0) { + if (IN_SET(errno, EINVAL, ENOSYS) && dev_scsi->use_sg == 4) { + dev_scsi->use_sg = 3; + goto resend; + } + log_debug_errno(errno, "%s: ioctl failed: %m", dev_scsi->kernel); + goto error; + } + + if (dev_scsi->use_sg == 4) + retval = sg_err_category4(io_buf); + else + retval = sg_err_category3(io_buf); + + switch (retval) { + case SG_ERR_CAT_NOTSUPPORTED: + buf[1] = 0; + _fallthrough_; + case SG_ERR_CAT_CLEAN: + case SG_ERR_CAT_RECOVERED: + retval = 0; + break; + + default: + if (dev_scsi->use_sg == 4) + retval = scsi_dump_v4(dev_scsi, io_buf); + else + retval = scsi_dump(dev_scsi, io_buf); + } + + if (!retval) { + retval = buflen; + } else if (retval > 0) { + if (--retry > 0) + goto resend; + retval = -1; + } + +error: + if (retval < 0) + log_debug("%s: Unable to get INQUIRY vpd %d page 0x%x.", + dev_scsi->kernel, evpd, page); + + return retval; +} + +/* Get list of supported EVPD pages */ +static int do_scsi_page0_inquiry(struct scsi_id_device *dev_scsi, int fd, + unsigned char *buffer, unsigned len) { + int retval; + + memzero(buffer, len); + retval = scsi_inquiry(dev_scsi, fd, 1, 0x0, buffer, len); + if (retval < 0) + return 1; + + if (buffer[1] != 0) { + log_debug("%s: page 0 not available.", dev_scsi->kernel); + return 1; + } + if (buffer[3] > len) { + log_debug("%s: page 0 buffer too long %d", dev_scsi->kernel, buffer[3]); + return 1; + } + + /* + * Following check is based on code once included in the 2.5.x + * kernel. + * + * Some ill behaved devices return the standard inquiry here + * rather than the evpd data, snoop the data to verify. + */ + if (buffer[3] > MODEL_LENGTH) { + /* + * If the vendor id appears in the page assume the page is + * invalid. + */ + if (strneq((char*) buffer + VENDOR_LENGTH, dev_scsi->vendor, VENDOR_LENGTH)) { + log_debug("%s: invalid page0 data", dev_scsi->kernel); + return 1; + } + } + return 0; +} + +static int append_vendor_model( + const struct scsi_id_device *dev_scsi, + char buf[static VENDOR_LENGTH + MODEL_LENGTH]) { + + assert(dev_scsi); + assert(buf); + + if (strnlen(dev_scsi->vendor, VENDOR_LENGTH) != VENDOR_LENGTH) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: bad vendor string \"%s\"", + dev_scsi->kernel, dev_scsi->vendor); + if (strnlen(dev_scsi->model, MODEL_LENGTH) != MODEL_LENGTH) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: bad model string \"%s\"", + dev_scsi->kernel, dev_scsi->model); + memcpy(buf, dev_scsi->vendor, VENDOR_LENGTH); + memcpy(buf + VENDOR_LENGTH, dev_scsi->model, MODEL_LENGTH); + return VENDOR_LENGTH + MODEL_LENGTH; +} + +/* + * check_fill_0x83_id - check the page 0x83 id, if OK allocate and fill + * serial number. + */ +static int check_fill_0x83_id(struct scsi_id_device *dev_scsi, + unsigned char *page_83, + const struct scsi_id_search_values + *id_search, char *serial, char *serial_short, + int max_len, char *wwn, + char *wwn_vendor_extension, char *tgpt_group) { + int i, j, s, len; + + /* + * ASSOCIATION must be with the device (value 0) + * or with the target port for SCSI_ID_TGTPORT + */ + if ((page_83[1] & 0x30) == 0x10) { + if (id_search->id_type != SCSI_ID_TGTGROUP) + return 1; + } else if ((page_83[1] & 0x30) != 0) + return 1; + + if ((page_83[1] & 0x0f) != id_search->id_type) + return 1; + + /* + * Possibly check NAA sub-type. + */ + if ((id_search->naa_type != SCSI_ID_NAA_DONT_CARE) && + (id_search->naa_type != (page_83[4] & 0xf0) >> 4)) + return 1; + + /* + * Check for matching code set - ASCII or BINARY. + */ + if ((page_83[0] & 0x0f) != id_search->code_set) + return 1; + + /* + * page_83[3]: identifier length + */ + len = page_83[3]; + if ((page_83[0] & 0x0f) != SCSI_ID_ASCII) + /* + * If not ASCII, use two bytes for each binary value. + */ + len *= 2; + + /* + * Add one byte for the NUL termination, and one for the id_type. + */ + len += 2; + if (id_search->id_type == SCSI_ID_VENDOR_SPECIFIC) + len += VENDOR_LENGTH + MODEL_LENGTH; + + if (max_len < len) { + log_debug("%s: length %d too short - need %d", + dev_scsi->kernel, max_len, len); + return 1; + } + + if (id_search->id_type == SCSI_ID_TGTGROUP && tgpt_group != NULL) { + unsigned group; + + group = ((unsigned)page_83[6] << 8) | page_83[7]; + sprintf(tgpt_group,"%x", group); + return 1; + } + + serial[0] = hex_str[id_search->id_type]; + + /* + * For SCSI_ID_VENDOR_SPECIFIC prepend the vendor and model before + * the id since it is not unique across all vendors and models, + * this differs from SCSI_ID_T10_VENDOR, where the vendor is + * included in the identifier. + */ + if (id_search->id_type == SCSI_ID_VENDOR_SPECIFIC) + if (append_vendor_model(dev_scsi, serial + 1) < 0) + return 1; + + i = 4; /* offset to the start of the identifier */ + s = j = strlen(serial); + if ((page_83[0] & 0x0f) == SCSI_ID_ASCII) { + /* + * ASCII descriptor. + */ + while (i < (4 + page_83[3])) + serial[j++] = page_83[i++]; + } else { + /* + * Binary descriptor, convert to ASCII, using two bytes of + * ASCII for each byte in the page_83. + */ + while (i < (4 + page_83[3])) { + serial[j++] = hex_str[(page_83[i] & 0xf0) >> 4]; + serial[j++] = hex_str[page_83[i] & 0x0f]; + i++; + } + } + + strcpy(serial_short, serial + s); + + if (id_search->id_type == SCSI_ID_NAA && wwn != NULL) { + strncpy(wwn, serial + s, 16); + if (wwn_vendor_extension) + strncpy(wwn_vendor_extension, serial + s + 16, 16); + } + + return 0; +} + +/* Extract the raw binary from VPD 0x83 pre-SPC devices */ +static int check_fill_0x83_prespc3(struct scsi_id_device *dev_scsi, + unsigned char *page_83, + const struct scsi_id_search_values + *id_search, char *serial, char *serial_short, int max_len) { + int i, j; + + serial[0] = hex_str[SCSI_ID_NAA]; + /* serial has been memset to zero before */ + j = strlen(serial); /* j = 1; */ + + for (i = 0; (i < page_83[3]) && (j < max_len-3); ++i) { + serial[j++] = hex_str[(page_83[4+i] & 0xf0) >> 4]; + serial[j++] = hex_str[ page_83[4+i] & 0x0f]; + } + serial[max_len-1] = 0; + strncpy(serial_short, serial, max_len-1); + return 0; +} + +/* Get device identification VPD page */ +static int do_scsi_page83_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int len, + char *unit_serial_number, char *wwn, + char *wwn_vendor_extension, char *tgpt_group) { + int retval; + unsigned id_ind, j; + unsigned char page_83[SCSI_INQ_BUFF_LEN]; + + /* also pick up the page 80 serial number */ + do_scsi_page80_inquiry(dev_scsi, fd, NULL, unit_serial_number, MAX_SERIAL_LEN); + + memzero(page_83, SCSI_INQ_BUFF_LEN); + retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_83, page_83, + SCSI_INQ_BUFF_LEN); + if (retval < 0) + return 1; + + if (page_83[1] != PAGE_83) { + log_debug("%s: Invalid page 0x83", dev_scsi->kernel); + return 1; + } + + /* + * XXX Some devices (IBM 3542) return all spaces for an identifier if + * the LUN is not actually configured. This leads to identifiers of + * the form: "1 ". + */ + + /* + * Model 4, 5, and (some) model 6 EMC Symmetrix devices return + * a page 83 reply according to SCSI-2 format instead of SPC-2/3. + * + * The SCSI-2 page 83 format returns an IEEE WWN in binary + * encoded hexi-decimal in the 16 bytes following the initial + * 4-byte page 83 reply header. + * + * Both the SPC-2 and SPC-3 formats return an IEEE WWN as part + * of an Identification descriptor. The 3rd byte of the first + * Identification descriptor is a reserved (BSZ) byte field. + * + * Reference the 7th byte of the page 83 reply to determine + * whether the reply is compliant with SCSI-2 or SPC-2/3 + * specifications. A zero value in the 7th byte indicates + * an SPC-2/3 conformant reply, (i.e., the reserved field of the + * first Identification descriptor). This byte will be non-zero + * for a SCSI-2 conformant page 83 reply from these EMC + * Symmetrix models since the 7th byte of the reply corresponds + * to the 4th and 5th nibbles of the 6-byte OUI for EMC, that is, + * 0x006048. + */ + + if (page_83[6] != 0) + return check_fill_0x83_prespc3(dev_scsi, page_83, id_search_list, + serial, serial_short, len); + + /* + * Search for a match in the prioritized id_search_list - since WWN ids + * come first we can pick up the WWN in check_fill_0x83_id(). + */ + for (id_ind = 0; + id_ind < sizeof(id_search_list)/sizeof(id_search_list[0]); + id_ind++) { + /* + * Examine each descriptor returned. There is normally only + * one or a small number of descriptors. + */ + for (j = 4; j <= ((unsigned)page_83[2] << 8) + (unsigned)page_83[3] + 3; j += page_83[j + 3] + 4) { + retval = check_fill_0x83_id(dev_scsi, page_83 + j, + id_search_list + id_ind, + serial, serial_short, len, + wwn, wwn_vendor_extension, + tgpt_group); + if (!retval) + return retval; + else if (retval < 0) + return retval; + } + } + return 1; +} + +/* + * Get device identification VPD page for older SCSI-2 device which is not + * compliant with either SPC-2 or SPC-3 format. + * + * Return the hard coded error code value 2 if the page 83 reply is not + * conformant to the SCSI-2 format. + */ +static int do_scsi_page83_prespc3_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int len) { + int retval; + int i, j; + unsigned char page_83[SCSI_INQ_BUFF_LEN]; + + memzero(page_83, SCSI_INQ_BUFF_LEN); + retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_83, page_83, SCSI_INQ_BUFF_LEN); + if (retval < 0) + return 1; + + if (page_83[1] != PAGE_83) { + log_debug("%s: Invalid page 0x83", dev_scsi->kernel); + return 1; + } + /* + * Model 4, 5, and (some) model 6 EMC Symmetrix devices return + * a page 83 reply according to SCSI-2 format instead of SPC-2/3. + * + * The SCSI-2 page 83 format returns an IEEE WWN in binary + * encoded hexi-decimal in the 16 bytes following the initial + * 4-byte page 83 reply header. + * + * Both the SPC-2 and SPC-3 formats return an IEEE WWN as part + * of an Identification descriptor. The 3rd byte of the first + * Identification descriptor is a reserved (BSZ) byte field. + * + * Reference the 7th byte of the page 83 reply to determine + * whether the reply is compliant with SCSI-2 or SPC-2/3 + * specifications. A zero value in the 7th byte indicates + * an SPC-2/3 conformant reply, (i.e., the reserved field of the + * first Identification descriptor). This byte will be non-zero + * for a SCSI-2 conformant page 83 reply from these EMC + * Symmetrix models since the 7th byte of the reply corresponds + * to the 4th and 5th nibbles of the 6-byte OUI for EMC, that is, + * 0x006048. + */ + if (page_83[6] == 0) + return 2; + + serial[0] = hex_str[SCSI_ID_NAA]; + /* + * The first four bytes contain data, not a descriptor. + */ + i = 4; + j = strlen(serial); + /* + * Binary descriptor, convert to ASCII, + * using two bytes of ASCII for each byte + * in the page_83. + */ + while (i < (page_83[3]+4)) { + serial[j++] = hex_str[(page_83[i] & 0xf0) >> 4]; + serial[j++] = hex_str[page_83[i] & 0x0f]; + i++; + } + return 0; +} + +/* Get unit serial number VPD page */ +static int do_scsi_page80_inquiry(struct scsi_id_device *dev_scsi, int fd, + char *serial, char *serial_short, int max_len) { + int retval; + int ser_ind; + int i; + int len; + unsigned char buf[SCSI_INQ_BUFF_LEN]; + + memzero(buf, SCSI_INQ_BUFF_LEN); + retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_80, buf, SCSI_INQ_BUFF_LEN); + if (retval < 0) + return retval; + + if (buf[1] != PAGE_80) { + log_debug("%s: Invalid page 0x80", dev_scsi->kernel); + return 1; + } + + len = 1 + VENDOR_LENGTH + MODEL_LENGTH + buf[3]; + if (max_len < len) { + log_debug("%s: length %d too short - need %d", + dev_scsi->kernel, max_len, len); + return 1; + } + /* + * Prepend 'S' to avoid unlikely collision with page 0x83 vendor + * specific type where we prepend '0' + vendor + model. + */ + len = buf[3]; + if (serial) { + serial[0] = 'S'; + ser_ind = append_vendor_model(dev_scsi, serial + 1); + if (ser_ind < 0) + return 1; + ser_ind++; /* for the leading 'S' */ + for (i = 4; i < len + 4; i++, ser_ind++) + serial[ser_ind] = buf[i]; + } + if (serial_short) { + memcpy(serial_short, buf + 4, len); + serial_short[len] = '\0'; + } + return 0; +} + +int scsi_std_inquiry(struct scsi_id_device *dev_scsi, const char *devname) { + int fd; + unsigned char buf[SCSI_INQ_BUFF_LEN]; + struct stat statbuf; + int err = 0; + + fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) { + log_debug_errno(errno, "scsi_id: cannot open %s: %m", devname); + return 1; + } + + if (fstat(fd, &statbuf) < 0) { + log_debug_errno(errno, "scsi_id: cannot stat %s: %m", devname); + err = 2; + goto out; + } + sprintf(dev_scsi->kernel,"%d:%d", major(statbuf.st_rdev), + minor(statbuf.st_rdev)); + + memzero(buf, SCSI_INQ_BUFF_LEN); + err = scsi_inquiry(dev_scsi, fd, 0, 0, buf, SCSI_INQ_BUFF_LEN); + if (err < 0) + goto out; + + err = 0; + memcpy(dev_scsi->vendor, buf + 8, 8); + dev_scsi->vendor[8] = '\0'; + memcpy(dev_scsi->model, buf + 16, 16); + dev_scsi->model[16] = '\0'; + memcpy(dev_scsi->revision, buf + 32, 4); + dev_scsi->revision[4] = '\0'; + sprintf(dev_scsi->type,"%x", buf[0] & 0x1f); + +out: + close(fd); + return err; +} + +int scsi_get_serial(struct scsi_id_device *dev_scsi, const char *devname, + int page_code, int len) { + unsigned char page0[SCSI_INQ_BUFF_LEN]; + int fd = -1; + int cnt; + int ind; + int retval; + + memzero(dev_scsi->serial, len); + initialize_srand(); + for (cnt = 20; cnt > 0; cnt--) { + struct timespec duration; + + fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0 || errno != EBUSY) + break; + duration.tv_sec = 0; + duration.tv_nsec = (200 * 1000 * 1000) + (rand() % 100 * 1000 * 1000); + nanosleep(&duration, NULL); + } + if (fd < 0) + return 1; + + if (page_code == PAGE_80) { + if (do_scsi_page80_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len)) { + retval = 1; + goto completed; + } else { + retval = 0; + goto completed; + } + } else if (page_code == PAGE_83) { + if (do_scsi_page83_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) { + retval = 1; + goto completed; + } else { + retval = 0; + goto completed; + } + } else if (page_code == PAGE_83_PRE_SPC3) { + retval = do_scsi_page83_prespc3_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len); + if (retval) { + /* + * Fallback to servicing a SPC-2/3 compliant page 83 + * inquiry if the page 83 reply format does not + * conform to pre-SPC3 expectations. + */ + if (retval == 2) { + if (do_scsi_page83_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) { + retval = 1; + goto completed; + } else { + retval = 0; + goto completed; + } + } + else { + retval = 1; + goto completed; + } + } else { + retval = 0; + goto completed; + } + } else if (page_code != 0x00) { + log_debug("%s: unsupported page code 0x%d", dev_scsi->kernel, page_code); + retval = 1; + goto completed; + } + + /* + * Get page 0, the page of the pages. By default, try from best to + * worst of supported pages: 0x83 then 0x80. + */ + if (do_scsi_page0_inquiry(dev_scsi, fd, page0, SCSI_INQ_BUFF_LEN)) { + /* + * Don't try anything else. Black list if a specific page + * should be used for this vendor+model, or maybe have an + * optional fall-back to page 0x80 or page 0x83. + */ + retval = 1; + goto completed; + } + + for (ind = 4; ind <= page0[3] + 3; ind++) + if (page0[ind] == PAGE_83) + if (!do_scsi_page83_inquiry(dev_scsi, fd, + dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) { + /* + * Success + */ + retval = 0; + goto completed; + } + + for (ind = 4; ind <= page0[3] + 3; ind++) + if (page0[ind] == PAGE_80) + if (!do_scsi_page80_inquiry(dev_scsi, fd, + dev_scsi->serial, dev_scsi->serial_short, len)) { + /* + * Success + */ + retval = 0; + goto completed; + } + retval = 1; + +completed: + close(fd); + return retval; +} diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c new file mode 100644 index 0000000..3f64548 --- /dev/null +++ b/src/udev/udev-builtin-blkid.c @@ -0,0 +1,317 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * probe disks for filesystems and partitions + * + * Copyright © 2011 Karel Zak <kzak@redhat.com> + */ + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "blkid-util.h" +#include "device-util.h" +#include "efi-loader.h" +#include "errno-util.h" +#include "fd-util.h" +#include "gpt.h" +#include "parse-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +static void print_property(sd_device *dev, bool test, const char *name, const char *value) { + char s[256]; + + s[0] = '\0'; + + if (streq(name, "TYPE")) { + udev_builtin_add_property(dev, test, "ID_FS_TYPE", value); + + } else if (streq(name, "USAGE")) { + udev_builtin_add_property(dev, test, "ID_FS_USAGE", value); + + } else if (streq(name, "VERSION")) { + udev_builtin_add_property(dev, test, "ID_FS_VERSION", value); + + } else if (streq(name, "UUID")) { + blkid_safe_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID", s); + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID_ENC", s); + + } else if (streq(name, "UUID_SUB")) { + blkid_safe_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID_SUB", s); + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_UUID_SUB_ENC", s); + + } else if (streq(name, "LABEL")) { + blkid_safe_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_LABEL", s); + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_LABEL_ENC", s); + + } else if (streq(name, "PTTYPE")) { + udev_builtin_add_property(dev, test, "ID_PART_TABLE_TYPE", value); + + } else if (streq(name, "PTUUID")) { + udev_builtin_add_property(dev, test, "ID_PART_TABLE_UUID", value); + + } else if (streq(name, "PART_ENTRY_NAME")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_PART_ENTRY_NAME", s); + + } else if (streq(name, "PART_ENTRY_TYPE")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_PART_ENTRY_TYPE", s); + + } else if (startswith(name, "PART_ENTRY_")) { + strscpyl(s, sizeof(s), "ID_", name, NULL); + udev_builtin_add_property(dev, test, s, value); + + } else if (streq(name, "SYSTEM_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_SYSTEM_ID", s); + + } else if (streq(name, "PUBLISHER_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_PUBLISHER_ID", s); + + } else if (streq(name, "APPLICATION_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_APPLICATION_ID", s); + + } else if (streq(name, "BOOT_SYSTEM_ID")) { + blkid_encode_string(value, s, sizeof(s)); + udev_builtin_add_property(dev, test, "ID_FS_BOOT_SYSTEM_ID", s); + } +} + +static int find_gpt_root(sd_device *dev, blkid_probe pr, bool test) { + +#if defined(GPT_ROOT_NATIVE) && ENABLE_EFI + + _cleanup_free_ char *root_id = NULL; + bool found_esp = false; + blkid_partlist pl; + int i, nvals, r; + + assert(pr); + + /* Iterate through the partitions on this disk, and see if the + * EFI ESP we booted from is on it. If so, find the first root + * disk, and add a property indicating its partition UUID. */ + + errno = 0; + pl = blkid_probe_get_partitions(pr); + if (!pl) + return errno_or_else(ENOMEM); + + nvals = blkid_partlist_numof_partitions(pl); + for (i = 0; i < nvals; i++) { + blkid_partition pp; + const char *stype, *sid; + sd_id128_t type; + + pp = blkid_partlist_get_partition(pl, i); + if (!pp) + continue; + + sid = blkid_partition_get_uuid(pp); + if (!sid) + continue; + + stype = blkid_partition_get_type_string(pp); + if (!stype) + continue; + + if (sd_id128_from_string(stype, &type) < 0) + continue; + + if (sd_id128_equal(type, GPT_ESP)) { + sd_id128_t id, esp; + + /* We found an ESP, let's see if it matches + * the ESP we booted from. */ + + if (sd_id128_from_string(sid, &id) < 0) + continue; + + r = efi_loader_get_device_part_uuid(&esp); + if (r < 0) + return r; + + if (sd_id128_equal(id, esp)) + found_esp = true; + + } else if (sd_id128_equal(type, GPT_ROOT_NATIVE)) { + unsigned long long flags; + + flags = blkid_partition_get_flags(pp); + if (flags & GPT_FLAG_NO_AUTO) + continue; + + /* We found a suitable root partition, let's + * remember the first one. */ + + if (!root_id) { + root_id = strdup(sid); + if (!root_id) + return -ENOMEM; + } + } + } + + /* We found the ESP on this disk, and also found a root + * partition, nice! Let's export its UUID */ + if (found_esp && root_id) + udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT_UUID", root_id); +#endif + + return 0; +} + +static int probe_superblocks(blkid_probe pr) { + struct stat st; + int rc; + + /* TODO: Return negative errno. */ + + if (fstat(blkid_probe_get_fd(pr), &st)) + return -errno; + + blkid_probe_enable_partitions(pr, 1); + + if (!S_ISCHR(st.st_mode) && + blkid_probe_get_size(pr) <= 1024 * 1440 && + blkid_probe_is_wholedisk(pr)) { + /* + * check if the small disk is partitioned, if yes then + * don't probe for filesystems. + */ + blkid_probe_enable_superblocks(pr, 0); + + rc = blkid_do_fullprobe(pr); + if (rc < 0) + return rc; /* -1 = error, 1 = nothing, 0 = success */ + + if (blkid_probe_lookup_value(pr, "PTTYPE", NULL, NULL) == 0) + return 0; /* partition table detected */ + } + + blkid_probe_set_partitions_flags(pr, BLKID_PARTS_ENTRY_DETAILS); + blkid_probe_enable_superblocks(pr, 1); + + return blkid_do_safeprobe(pr); +} + +static int builtin_blkid(sd_device *dev, int argc, char *argv[], bool test) { + const char *devnode, *root_partition = NULL, *data, *name; + _cleanup_(blkid_free_probep) blkid_probe pr = NULL; + bool noraid = false, is_gpt = false; + _cleanup_close_ int fd = -1; + int64_t offset = 0; + int nvals, i, r; + + static const struct option options[] = { + { "offset", required_argument, NULL, 'o' }, + { "noraid", no_argument, NULL, 'R' }, + {} + }; + + for (;;) { + int option; + + option = getopt_long(argc, argv, "o:R", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'o': + r = safe_atoi64(optarg, &offset); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to parse '%s' as an integer: %m", optarg); + if (offset < 0) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(ERANGE), "Invalid offset %"PRIi64": %m", offset); + break; + case 'R': + noraid = true; + break; + } + } + + errno = 0; + pr = blkid_new_probe(); + if (!pr) + return log_device_debug_errno(dev, errno > 0 ? errno : ENOMEM, "Failed to create blkid prober: %m"); + + blkid_probe_set_superblocks_flags(pr, + BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID | + BLKID_SUBLKS_TYPE | BLKID_SUBLKS_SECTYPE | + BLKID_SUBLKS_USAGE | BLKID_SUBLKS_VERSION); + + if (noraid) + blkid_probe_filter_superblocks_usage(pr, BLKID_FLTR_NOTIN, BLKID_USAGE_RAID); + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device name: %m"); + + fd = open(devnode, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (fd < 0) + return log_device_debug_errno(dev, errno, "Failed to open block device %s: %m", devnode); + + errno = 0; + r = blkid_probe_set_device(pr, fd, offset, 0); + if (r < 0) + return log_device_debug_errno(dev, errno > 0 ? errno : ENOMEM, "Failed to set device to blkid prober: %m"); + + log_device_debug(dev, "Probe %s with %sraid and offset=%"PRIi64, devnode, noraid ? "no" : "", offset); + + r = probe_superblocks(pr); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to probe superblocks: %m"); + + /* If the device is a partition then its parent passed the root partition UUID to the device */ + (void) sd_device_get_property_value(dev, "ID_PART_GPT_AUTO_ROOT_UUID", &root_partition); + + errno = 0; + nvals = blkid_probe_numof_values(pr); + if (nvals < 0) + return log_device_debug_errno(dev, errno > 0 ? errno : ENOMEM, "Failed to get number of probed values: %m"); + + for (i = 0; i < nvals; i++) { + if (blkid_probe_get_value(pr, i, &name, &data, NULL) < 0) + continue; + + print_property(dev, test, name, data); + + /* Is this a disk with GPT partition table? */ + if (streq(name, "PTTYPE") && streq(data, "gpt")) + is_gpt = true; + + /* Is this a partition that matches the root partition + * property inherited from the parent? */ + if (root_partition && streq(name, "PART_ENTRY_UUID") && streq(data, root_partition)) + udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT", "1"); + } + + if (is_gpt) + find_gpt_root(dev, pr, test); + + return 0; +} + +const UdevBuiltin udev_builtin_blkid = { + .name = "blkid", + .cmd = builtin_blkid, + .help = "Filesystem and partition probing", + .run_once = true, +}; diff --git a/src/udev/udev-builtin-btrfs.c b/src/udev/udev-builtin-btrfs.c new file mode 100644 index 0000000..9079d1b --- /dev/null +++ b/src/udev/udev-builtin-btrfs.c @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <linux/btrfs.h> +#include <stdlib.h> +#include <sys/ioctl.h> + +#include "device-util.h" +#include "fd-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" +#include "util.h" + +static int builtin_btrfs(sd_device *dev, int argc, char *argv[], bool test) { + struct btrfs_ioctl_vol_args args = {}; + _cleanup_close_ int fd = -1; + int r; + + if (argc != 3 || !streq(argv[1], "ready")) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid arguments"); + + fd = open("/dev/btrfs-control", O_RDWR|O_CLOEXEC); + if (fd < 0) + return log_device_debug_errno(dev, errno, "Failed to open /dev/btrfs-control: %m"); + + strscpy(args.name, sizeof(args.name), argv[2]); + r = ioctl(fd, BTRFS_IOC_DEVICES_READY, &args); + if (r < 0) + return log_device_debug_errno(dev, errno, "Failed to call BTRFS_IOC_DEVICES_READY: %m"); + + udev_builtin_add_property(dev, test, "ID_BTRFS_READY", one_zero(r == 0)); + return 0; +} + +const UdevBuiltin udev_builtin_btrfs = { + .name = "btrfs", + .cmd = builtin_btrfs, + .help = "btrfs volume management", +}; diff --git a/src/udev/udev-builtin-hwdb.c b/src/udev/udev-builtin-hwdb.c new file mode 100644 index 0000000..7883518 --- /dev/null +++ b/src/udev/udev-builtin-hwdb.c @@ -0,0 +1,221 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fnmatch.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> + +#include "sd-hwdb.h" + +#include "alloc-util.h" +#include "device-util.h" +#include "hwdb-util.h" +#include "parse-util.h" +#include "string-util.h" +#include "udev-builtin.h" + +static sd_hwdb *hwdb; + +int udev_builtin_hwdb_lookup(sd_device *dev, + const char *prefix, const char *modalias, + const char *filter, bool test) { + _cleanup_free_ char *lookup = NULL; + const char *key, *value; + int n = 0, r; + + if (!hwdb) + return -ENOENT; + + if (prefix) { + lookup = strjoin(prefix, modalias); + if (!lookup) + return -ENOMEM; + modalias = lookup; + } + + SD_HWDB_FOREACH_PROPERTY(hwdb, modalias, key, value) { + if (filter && fnmatch(filter, key, FNM_NOESCAPE) != 0) + continue; + + r = udev_builtin_add_property(dev, test, key, value); + if (r < 0) + return r; + n++; + } + return n; +} + +static const char *modalias_usb(sd_device *dev, char *s, size_t size) { + const char *v, *p, *n = NULL; + uint16_t vn, pn; + + if (sd_device_get_sysattr_value(dev, "idVendor", &v) < 0) + return NULL; + if (sd_device_get_sysattr_value(dev, "idProduct", &p) < 0) + return NULL; + if (safe_atoux16(v, &vn) < 0) + return NULL; + if (safe_atoux16(p, &pn) < 0) + return NULL; + (void) sd_device_get_sysattr_value(dev, "product", &n); + + snprintf(s, size, "usb:v%04Xp%04X:%s", vn, pn, strempty(n)); + return s; +} + +static int udev_builtin_hwdb_search(sd_device *dev, sd_device *srcdev, + const char *subsystem, const char *prefix, + const char *filter, bool test) { + char s[LINE_MAX]; + bool last = false; + int r = 0; + + assert(dev); + + if (!srcdev) + srcdev = dev; + + for (sd_device *d = srcdev; d; ) { + const char *dsubsys, *devtype, *modalias = NULL; + + if (sd_device_get_subsystem(d, &dsubsys) < 0) + goto next; + + /* look only at devices of a specific subsystem */ + if (subsystem && !streq(dsubsys, subsystem)) + goto next; + + (void) sd_device_get_property_value(d, "MODALIAS", &modalias); + + if (streq(dsubsys, "usb") && + sd_device_get_devtype(d, &devtype) >= 0 && + streq(devtype, "usb_device")) { + /* if the usb_device does not have a modalias, compose one */ + if (!modalias) + modalias = modalias_usb(d, s, sizeof(s)); + + /* avoid looking at any parent device, they are usually just a USB hub */ + last = true; + } + + if (!modalias) + goto next; + + log_device_debug(dev, "hwdb modalias key: \"%s\"", modalias); + + r = udev_builtin_hwdb_lookup(dev, prefix, modalias, filter, test); + if (r > 0) + break; + + if (last) + break; +next: + if (sd_device_get_parent(d, &d) < 0) + break; + } + + return r; +} + +static int builtin_hwdb(sd_device *dev, int argc, char *argv[], bool test) { + static const struct option options[] = { + { "filter", required_argument, NULL, 'f' }, + { "device", required_argument, NULL, 'd' }, + { "subsystem", required_argument, NULL, 's' }, + { "lookup-prefix", required_argument, NULL, 'p' }, + {} + }; + const char *filter = NULL; + const char *device = NULL; + const char *subsystem = NULL; + const char *prefix = NULL; + _cleanup_(sd_device_unrefp) sd_device *srcdev = NULL; + int r; + + if (!hwdb) + return -EINVAL; + + for (;;) { + int option; + + option = getopt_long(argc, argv, "f:d:s:p:", options, NULL); + if (option == -1) + break; + + switch (option) { + case 'f': + filter = optarg; + break; + + case 'd': + device = optarg; + break; + + case 's': + subsystem = optarg; + break; + + case 'p': + prefix = optarg; + break; + } + } + + /* query a specific key given as argument */ + if (argv[optind]) { + r = udev_builtin_hwdb_lookup(dev, prefix, argv[optind], filter, test); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m"); + if (r == 0) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENODATA), "No entry found from hwdb."); + return r; + } + + /* read data from another device than the device we will store the data */ + if (device) { + r = sd_device_new_from_device_id(&srcdev, device); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to create sd_device object '%s': %m", device); + } + + r = udev_builtin_hwdb_search(dev, srcdev, subsystem, prefix, filter, test); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m"); + if (r == 0) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENODATA), "No entry found from hwdb."); + return r; +} + +/* called at udev startup and reload */ +static int builtin_hwdb_init(void) { + int r; + + if (hwdb) + return 0; + + r = sd_hwdb_new(&hwdb); + if (r < 0) + return r; + + return 0; +} + +/* called on udev shutdown and reload request */ +static void builtin_hwdb_exit(void) { + hwdb = sd_hwdb_unref(hwdb); +} + +/* called every couple of seconds during event activity; 'true' if config has changed */ +static bool builtin_hwdb_validate(void) { + return hwdb_validate(hwdb); +} + +const UdevBuiltin udev_builtin_hwdb = { + .name = "hwdb", + .cmd = builtin_hwdb, + .init = builtin_hwdb_init, + .exit = builtin_hwdb_exit, + .validate = builtin_hwdb_validate, + .help = "Hardware database", +}; diff --git a/src/udev/udev-builtin-input_id.c b/src/udev/udev-builtin-input_id.c new file mode 100644 index 0000000..6a4911c --- /dev/null +++ b/src/udev/udev-builtin-input_id.c @@ -0,0 +1,395 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * expose input properties via udev + * + * Portions Copyright © 2004 David Zeuthen, <david@fubar.dk> + * Copyright © 2014 Carlos Garnacho <carlosg@gnome.org> + */ + +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdlib.h> +#include <unistd.h> +#include <linux/limits.h> + +#include "device-util.h" +#include "fd-util.h" +#include "missing_input.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "udev-builtin.h" +#include "util.h" + +/* we must use this kernel-compatible implementation */ +#define BITS_PER_LONG (sizeof(unsigned long) * 8) +#define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1) +#define OFF(x) ((x)%BITS_PER_LONG) +#define BIT(x) (1UL<<OFF(x)) +#define LONG(x) ((x)/BITS_PER_LONG) +#define test_bit(bit, array) ((array[LONG(bit)] >> OFF(bit)) & 1) + +struct range { + unsigned start; + unsigned end; +}; + +/* key code ranges above BTN_MISC (start is inclusive, stop is exclusive)*/ +static const struct range high_key_blocks[] = { + { KEY_OK, BTN_DPAD_UP }, + { KEY_ALS_TOGGLE, BTN_TRIGGER_HAPPY } +}; + +static int abs_size_mm(const struct input_absinfo *absinfo) { + /* Resolution is defined to be in units/mm for ABS_X/Y */ + return (absinfo->maximum - absinfo->minimum) / absinfo->resolution; +} + +static void extract_info(sd_device *dev, const char *devpath, bool test) { + char width[DECIMAL_STR_MAX(int)], height[DECIMAL_STR_MAX(int)]; + struct input_absinfo xabsinfo = {}, yabsinfo = {}; + _cleanup_close_ int fd = -1; + + fd = open(devpath, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return; + + if (ioctl(fd, EVIOCGABS(ABS_X), &xabsinfo) < 0 || + ioctl(fd, EVIOCGABS(ABS_Y), &yabsinfo) < 0) + return; + + if (xabsinfo.resolution <= 0 || yabsinfo.resolution <= 0) + return; + + xsprintf(width, "%d", abs_size_mm(&xabsinfo)); + xsprintf(height, "%d", abs_size_mm(&yabsinfo)); + + udev_builtin_add_property(dev, test, "ID_INPUT_WIDTH_MM", width); + udev_builtin_add_property(dev, test, "ID_INPUT_HEIGHT_MM", height); +} + +/* + * Read a capability attribute and return bitmask. + * @param dev sd_device + * @param attr sysfs attribute name (e. g. "capabilities/key") + * @param bitmask: Output array which has a sizeof of bitmask_size + */ +static void get_cap_mask(sd_device *pdev, const char* attr, + unsigned long *bitmask, size_t bitmask_size, + bool test) { + const char *v; + char text[4096]; + unsigned i; + char* word; + unsigned long val; + + if (sd_device_get_sysattr_value(pdev, attr, &v) < 0) + v = ""; + + xsprintf(text, "%s", v); + log_device_debug(pdev, "%s raw kernel attribute: %s", attr, text); + + memzero(bitmask, bitmask_size); + i = 0; + while ((word = strrchr(text, ' ')) != NULL) { + val = strtoul(word+1, NULL, 16); + if (i < bitmask_size / sizeof(unsigned long)) + bitmask[i] = val; + else + log_device_debug(pdev, "Ignoring %s block %lX which is larger than maximum size", attr, val); + *word = '\0'; + ++i; + } + val = strtoul (text, NULL, 16); + if (i < bitmask_size / sizeof(unsigned long)) + bitmask[i] = val; + else + log_device_debug(pdev, "Ignoring %s block %lX which is larger than maximum size", attr, val); + + if (test) { + /* printf pattern with the right unsigned long number of hex chars */ + xsprintf(text, " bit %%4u: %%0%zulX\n", + 2 * sizeof(unsigned long)); + log_device_debug(pdev, "%s decoded bit map:", attr); + val = bitmask_size / sizeof (unsigned long); + /* skip over leading zeros */ + while (bitmask[val-1] == 0 && val > 0) + --val; + for (i = 0; i < val; ++i) { + DISABLE_WARNING_FORMAT_NONLITERAL; + log_device_debug(pdev, text, i * BITS_PER_LONG, bitmask[i]); + REENABLE_WARNING; + } + } +} + +static struct input_id get_input_id(sd_device *dev) { + const char *v; + struct input_id id = {}; + + if (sd_device_get_sysattr_value(dev, "id/bustype", &v) >= 0) + (void) safe_atoux16(v, &id.bustype); + if (sd_device_get_sysattr_value(dev, "id/vendor", &v) >= 0) + (void) safe_atoux16(v, &id.vendor); + if (sd_device_get_sysattr_value(dev, "id/product", &v) >= 0) + (void) safe_atoux16(v, &id.product); + if (sd_device_get_sysattr_value(dev, "id/version", &v) >= 0) + (void) safe_atoux16(v, &id.version); + + return id; +} + +/* pointer devices */ +static bool test_pointers(sd_device *dev, + const struct input_id *id, + const unsigned long* bitmask_ev, + const unsigned long* bitmask_abs, + const unsigned long* bitmask_key, + const unsigned long* bitmask_rel, + const unsigned long* bitmask_props, + bool test) { + int button, axis; + bool has_abs_coordinates = false; + bool has_rel_coordinates = false; + bool has_mt_coordinates = false; + bool has_joystick_axes_or_buttons = false; + bool has_pad_buttons = false; + bool is_direct = false; + bool has_touch = false; + bool has_3d_coordinates = false; + bool has_keys = false; + bool has_stylus = false; + bool has_pen = false; + bool finger_but_no_pen = false; + bool has_mouse_button = false; + bool is_mouse = false; + bool is_touchpad = false; + bool is_touchscreen = false; + bool is_tablet = false; + bool is_tablet_pad = false; + bool is_joystick = false; + bool is_accelerometer = false; + bool is_pointing_stick = false; + + has_keys = test_bit(EV_KEY, bitmask_ev); + has_abs_coordinates = test_bit(ABS_X, bitmask_abs) && test_bit(ABS_Y, bitmask_abs); + has_3d_coordinates = has_abs_coordinates && test_bit(ABS_Z, bitmask_abs); + is_accelerometer = test_bit(INPUT_PROP_ACCELEROMETER, bitmask_props); + + if (!has_keys && has_3d_coordinates) + is_accelerometer = true; + + if (is_accelerometer) { + udev_builtin_add_property(dev, test, "ID_INPUT_ACCELEROMETER", "1"); + return true; + } + + is_pointing_stick = test_bit(INPUT_PROP_POINTING_STICK, bitmask_props); + has_stylus = test_bit(BTN_STYLUS, bitmask_key); + has_pen = test_bit(BTN_TOOL_PEN, bitmask_key); + finger_but_no_pen = test_bit(BTN_TOOL_FINGER, bitmask_key) && !test_bit(BTN_TOOL_PEN, bitmask_key); + for (button = BTN_MOUSE; button < BTN_JOYSTICK && !has_mouse_button; button++) + has_mouse_button = test_bit(button, bitmask_key); + has_rel_coordinates = test_bit(EV_REL, bitmask_ev) && test_bit(REL_X, bitmask_rel) && test_bit(REL_Y, bitmask_rel); + has_mt_coordinates = test_bit(ABS_MT_POSITION_X, bitmask_abs) && test_bit(ABS_MT_POSITION_Y, bitmask_abs); + + /* unset has_mt_coordinates if devices claims to have all abs axis */ + if (has_mt_coordinates && test_bit(ABS_MT_SLOT, bitmask_abs) && test_bit(ABS_MT_SLOT - 1, bitmask_abs)) + has_mt_coordinates = false; + is_direct = test_bit(INPUT_PROP_DIRECT, bitmask_props); + has_touch = test_bit(BTN_TOUCH, bitmask_key); + has_pad_buttons = test_bit(BTN_0, bitmask_key) && has_stylus && !has_pen; + + /* joysticks don't necessarily have buttons; e. g. + * rudders/pedals are joystick-like, but buttonless; they have + * other fancy axes. Others have buttons only but no axes. + * + * The BTN_JOYSTICK range starts after the mouse range, so a mouse + * with more than 16 buttons runs into the joystick range (e.g. Mad + * Catz Mad Catz M.M.O.TE). Skip those. + */ + if (!test_bit(BTN_JOYSTICK - 1, bitmask_key)) { + for (button = BTN_JOYSTICK; button < BTN_DIGI && !has_joystick_axes_or_buttons; button++) + has_joystick_axes_or_buttons = test_bit(button, bitmask_key); + for (button = BTN_TRIGGER_HAPPY1; button <= BTN_TRIGGER_HAPPY40 && !has_joystick_axes_or_buttons; button++) + has_joystick_axes_or_buttons = test_bit(button, bitmask_key); + for (button = BTN_DPAD_UP; button <= BTN_DPAD_RIGHT && !has_joystick_axes_or_buttons; button++) + has_joystick_axes_or_buttons = test_bit(button, bitmask_key); + } + for (axis = ABS_RX; axis < ABS_PRESSURE && !has_joystick_axes_or_buttons; axis++) + has_joystick_axes_or_buttons = test_bit(axis, bitmask_abs); + + if (has_abs_coordinates) { + if (has_stylus || has_pen) + is_tablet = true; + else if (finger_but_no_pen && !is_direct) + is_touchpad = true; + else if (has_mouse_button) + /* This path is taken by VMware's USB mouse, which has + * absolute axes, but no touch/pressure button. */ + is_mouse = true; + else if (has_touch || is_direct) + is_touchscreen = true; + else if (has_joystick_axes_or_buttons) + is_joystick = true; + } else if (has_joystick_axes_or_buttons) + is_joystick = true; + + if (has_mt_coordinates) { + if (has_stylus || has_pen) + is_tablet = true; + else if (finger_but_no_pen && !is_direct) + is_touchpad = true; + else if (has_touch || is_direct) + is_touchscreen = true; + } + + if (is_tablet && has_pad_buttons) + is_tablet_pad = true; + + if (!is_tablet && !is_touchpad && !is_joystick && + has_mouse_button && + (has_rel_coordinates || + !has_abs_coordinates)) /* mouse buttons and no axis */ + is_mouse = true; + + /* There is no such thing as an i2c mouse */ + if (is_mouse && id->bustype == BUS_I2C) + is_pointing_stick = true; + + if (is_pointing_stick) + udev_builtin_add_property(dev, test, "ID_INPUT_POINTINGSTICK", "1"); + if (is_mouse) + udev_builtin_add_property(dev, test, "ID_INPUT_MOUSE", "1"); + if (is_touchpad) + udev_builtin_add_property(dev, test, "ID_INPUT_TOUCHPAD", "1"); + if (is_touchscreen) + udev_builtin_add_property(dev, test, "ID_INPUT_TOUCHSCREEN", "1"); + if (is_joystick) + udev_builtin_add_property(dev, test, "ID_INPUT_JOYSTICK", "1"); + if (is_tablet) + udev_builtin_add_property(dev, test, "ID_INPUT_TABLET", "1"); + if (is_tablet_pad) + udev_builtin_add_property(dev, test, "ID_INPUT_TABLET_PAD", "1"); + + return is_tablet || is_mouse || is_touchpad || is_touchscreen || is_joystick || is_pointing_stick; +} + +/* key like devices */ +static bool test_key(sd_device *dev, + const unsigned long* bitmask_ev, + const unsigned long* bitmask_key, + bool test) { + unsigned i; + unsigned long found; + unsigned long mask; + bool ret = false; + + /* do we have any KEY_* capability? */ + if (!test_bit(EV_KEY, bitmask_ev)) { + log_device_debug(dev, "test_key: no EV_KEY capability"); + return false; + } + + /* only consider KEY_* here, not BTN_* */ + found = 0; + for (i = 0; i < BTN_MISC/BITS_PER_LONG; ++i) { + found |= bitmask_key[i]; + log_device_debug(dev, "test_key: checking bit block %lu for any keys; found=%i", (unsigned long)i*BITS_PER_LONG, found > 0); + } + /* If there are no keys in the lower block, check the higher blocks */ + if (!found) { + unsigned block; + for (block = 0; block < (sizeof(high_key_blocks) / sizeof(struct range)); ++block) { + for (i = high_key_blocks[block].start; i < high_key_blocks[block].end; ++i) { + if (test_bit(i, bitmask_key)) { + log_device_debug(dev, "test_key: Found key %x in high block", i); + found = 1; + break; + } + } + } + } + + if (found > 0) { + udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1"); + ret = true; + } + + /* the first 32 bits are ESC, numbers, and Q to D; if we have all of + * those, consider it a full keyboard; do not test KEY_RESERVED, though */ + mask = 0xFFFFFFFE; + if (FLAGS_SET(bitmask_key[0], mask)) { + udev_builtin_add_property(dev, test, "ID_INPUT_KEYBOARD", "1"); + ret = true; + } + + return ret; +} + +static int builtin_input_id(sd_device *dev, int argc, char *argv[], bool test) { + sd_device *pdev; + unsigned long bitmask_ev[NBITS(EV_MAX)]; + unsigned long bitmask_abs[NBITS(ABS_MAX)]; + unsigned long bitmask_key[NBITS(KEY_MAX)]; + unsigned long bitmask_rel[NBITS(REL_MAX)]; + unsigned long bitmask_props[NBITS(INPUT_PROP_MAX)]; + const char *sysname, *devnode; + bool is_pointer; + bool is_key; + + assert(dev); + + /* walk up the parental chain until we find the real input device; the + * argument is very likely a subdevice of this, like eventN */ + for (pdev = dev; pdev; ) { + const char *s; + + if (sd_device_get_sysattr_value(pdev, "capabilities/ev", &s) >= 0) + break; + + if (sd_device_get_parent_with_subsystem_devtype(pdev, "input", NULL, &pdev) >= 0) + continue; + + pdev = NULL; + break; + } + + if (pdev) { + struct input_id id = get_input_id(pdev); + + /* Use this as a flag that input devices were detected, so that this + * program doesn't need to be called more than once per device */ + udev_builtin_add_property(dev, test, "ID_INPUT", "1"); + get_cap_mask(pdev, "capabilities/ev", bitmask_ev, sizeof(bitmask_ev), test); + get_cap_mask(pdev, "capabilities/abs", bitmask_abs, sizeof(bitmask_abs), test); + get_cap_mask(pdev, "capabilities/rel", bitmask_rel, sizeof(bitmask_rel), test); + get_cap_mask(pdev, "capabilities/key", bitmask_key, sizeof(bitmask_key), test); + get_cap_mask(pdev, "properties", bitmask_props, sizeof(bitmask_props), test); + is_pointer = test_pointers(dev, &id, bitmask_ev, bitmask_abs, + bitmask_key, bitmask_rel, + bitmask_props, test); + is_key = test_key(dev, bitmask_ev, bitmask_key, test); + /* Some evdev nodes have only a scrollwheel */ + if (!is_pointer && !is_key && test_bit(EV_REL, bitmask_ev) && + (test_bit(REL_WHEEL, bitmask_rel) || test_bit(REL_HWHEEL, bitmask_rel))) + udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1"); + if (test_bit(EV_SW, bitmask_ev)) + udev_builtin_add_property(dev, test, "ID_INPUT_SWITCH", "1"); + + } + + if (sd_device_get_devname(dev, &devnode) >= 0 && + sd_device_get_sysname(dev, &sysname) >= 0 && + startswith(sysname, "event")) + extract_info(dev, devnode, test); + + return 0; +} + +const UdevBuiltin udev_builtin_input_id = { + .name = "input_id", + .cmd = builtin_input_id, + .help = "Input device properties", +}; diff --git a/src/udev/udev-builtin-keyboard.c b/src/udev/udev-builtin-keyboard.c new file mode 100644 index 0000000..cd766a8 --- /dev/null +++ b/src/udev/udev-builtin-keyboard.c @@ -0,0 +1,254 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <linux/input.h> + +#include "device-util.h" +#include "fd-util.h" +#include "parse-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +static const struct key_name *keyboard_lookup_key(const char *str, GPERF_LEN_TYPE len); +#include "keyboard-keys-from-name.h" + +static int install_force_release(sd_device *dev, const unsigned *release, unsigned release_count) { + sd_device *atkbd; + const char *cur; + char codes[4096]; + char *s; + size_t l; + unsigned i; + int r; + + assert(dev); + assert(release); + + r = sd_device_get_parent_with_subsystem_devtype(dev, "serio", NULL, &atkbd); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get serio parent: %m"); + + r = sd_device_get_sysattr_value(atkbd, "force_release", &cur); + if (r < 0) + return log_device_error_errno(atkbd, r, "Failed to get force-release attribute: %m"); + + s = codes; + l = sizeof(codes); + + /* copy current content */ + l = strpcpy(&s, l, cur); + + /* append new codes */ + for (i = 0; i < release_count; i++) + l = strpcpyf(&s, l, ",%u", release[i]); + + log_device_debug(atkbd, "keyboard: updating force-release list with '%s'", codes); + r = sd_device_set_sysattr_value(atkbd, "force_release", codes); + if (r < 0) + return log_device_error_errno(atkbd, r, "Failed to set force-release attribute: %m"); + + return 0; +} + +static int map_keycode(sd_device *dev, int fd, int scancode, const char *keycode) { + struct { + unsigned scan; + unsigned key; + } map; + char *endptr; + const struct key_name *k; + unsigned keycode_num; + + /* translate identifier to key code */ + k = keyboard_lookup_key(keycode, strlen(keycode)); + if (k) { + keycode_num = k->id; + } else { + /* check if it's a numeric code already */ + keycode_num = strtoul(keycode, &endptr, 0); + if (endptr[0] !='\0') + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Failed to parse key identifier '%s'", keycode); + } + + map.scan = scancode; + map.key = keycode_num; + + log_device_debug(dev, "keyboard: mapping scan code %d (0x%x) to key code %d (0x%x)", + map.scan, map.scan, map.key, map.key); + + if (ioctl(fd, EVIOCSKEYCODE, &map) < 0) + return log_device_error_errno(dev, errno, "Failed to call EVIOCSKEYCODE with scan code 0x%x, and key code %d: %m", map.scan, map.key); + + return 0; +} + +static const char* parse_token(const char *current, int32_t *val_out) { + char *next; + int32_t val; + + if (!current) + return NULL; + + val = strtol(current, &next, 0); + if (*next && *next != ':') + return NULL; + + if (next != current) + *val_out = val; + + if (*next) + next++; + + return next; +} + +static int override_abs(sd_device *dev, int fd, unsigned evcode, const char *value) { + struct input_absinfo absinfo; + const char *next; + + if (ioctl(fd, EVIOCGABS(evcode), &absinfo) < 0) + return log_device_error_errno(dev, errno, "Failed to call EVIOCGABS"); + + next = parse_token(value, &absinfo.minimum); + next = parse_token(next, &absinfo.maximum); + next = parse_token(next, &absinfo.resolution); + next = parse_token(next, &absinfo.fuzz); + next = parse_token(next, &absinfo.flat); + if (!next) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), + "Failed to parse EV_ABS override '%s'", value); + + log_device_debug(dev, "keyboard: %x overridden with %"PRIi32"/%"PRIi32"/%"PRIi32"/%"PRIi32"/%"PRIi32, + evcode, absinfo.minimum, absinfo.maximum, absinfo.resolution, absinfo.fuzz, absinfo.flat); + if (ioctl(fd, EVIOCSABS(evcode), &absinfo) < 0) + return log_device_error_errno(dev, errno, "Failed to call EVIOCSABS"); + + return 0; +} + +static int set_trackpoint_sensitivity(sd_device *dev, const char *value) { + sd_device *pdev; + char val_s[DECIMAL_STR_MAX(int)]; + int r, val_i; + + assert(dev); + assert(value); + + /* The sensitivity sysfs attr belongs to the serio parent device */ + r = sd_device_get_parent_with_subsystem_devtype(dev, "serio", NULL, &pdev); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get serio parent: %m"); + + r = safe_atoi(value, &val_i); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to parse POINTINGSTICK_SENSITIVITY '%s': %m", value); + else if (val_i < 0 || val_i > 255) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(ERANGE), "POINTINGSTICK_SENSITIVITY %d outside range [0..255]", val_i); + + xsprintf(val_s, "%d", val_i); + + r = sd_device_set_sysattr_value(pdev, "sensitivity", val_s); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to write 'sensitivity' attribute: %m"); + + return 0; +} + +static int builtin_keyboard(sd_device *dev, int argc, char *argv[], bool test) { + unsigned release[1024]; + unsigned release_count = 0; + _cleanup_close_ int fd = -1; + const char *node, *key, *value; + int has_abs = -1, r; + + r = sd_device_get_devname(dev, &node); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get device name: %m"); + + FOREACH_DEVICE_PROPERTY(dev, key, value) { + char *endptr; + + if (startswith(key, "KEYBOARD_KEY_")) { + const char *keycode = value; + unsigned scancode; + + /* KEYBOARD_KEY_<hex scan code>=<key identifier string> */ + scancode = strtoul(key + 13, &endptr, 16); + if (endptr[0] != '\0') { + log_device_warning(dev, "Failed to parse scan code from \"%s\", ignoring", key); + continue; + } + + /* a leading '!' needs a force-release entry */ + if (keycode[0] == '!') { + keycode++; + + release[release_count] = scancode; + if (release_count < ELEMENTSOF(release)-1) + release_count++; + + if (keycode[0] == '\0') + continue; + } + + if (fd < 0) { + fd = open(node, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return log_device_error_errno(dev, errno, "Failed to open device '%s': %m", node); + } + + (void) map_keycode(dev, fd, scancode, keycode); + } else if (startswith(key, "EVDEV_ABS_")) { + unsigned evcode; + + /* EVDEV_ABS_<EV_ABS code>=<min>:<max>:<res>:<fuzz>:<flat> */ + evcode = strtoul(key + 10, &endptr, 16); + if (endptr[0] != '\0') { + log_device_warning(dev, "Failed to parse EV_ABS code from \"%s\", ignoring", key); + continue; + } + + if (fd < 0) { + fd = open(node, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return log_device_error_errno(dev, errno, "Failed to open device '%s': %m", node); + } + + if (has_abs == -1) { + unsigned long bits; + int rc; + + rc = ioctl(fd, EVIOCGBIT(0, sizeof(bits)), &bits); + if (rc < 0) + return log_device_error_errno(dev, errno, "Failed to set EVIOCGBIT"); + + has_abs = !!(bits & (1 << EV_ABS)); + if (!has_abs) + log_device_warning(dev, "EVDEV_ABS override set but no EV_ABS present on device"); + } + + if (!has_abs) + continue; + + (void) override_abs(dev, fd, evcode, value); + } else if (streq(key, "POINTINGSTICK_SENSITIVITY")) + (void) set_trackpoint_sensitivity(dev, value); + } + + /* install list of force-release codes */ + if (release_count > 0) + (void) install_force_release(dev, release, release_count); + + return 0; +} + +const UdevBuiltin udev_builtin_keyboard = { + .name = "keyboard", + .cmd = builtin_keyboard, + .help = "Keyboard scan code to key mapping", +}; diff --git a/src/udev/udev-builtin-kmod.c b/src/udev/udev-builtin-kmod.c new file mode 100644 index 0000000..3be8bd5 --- /dev/null +++ b/src/udev/udev-builtin-kmod.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * load kernel modules + * + * Copyright © 2011 ProFUSION embedded systems + */ + +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "module-util.h" +#include "string-util.h" +#include "udev-builtin.h" + +static struct kmod_ctx *ctx = NULL; + +_printf_(6,0) static void udev_kmod_log(void *data, int priority, const char *file, int line, const char *fn, const char *format, va_list args) { + log_internalv(priority, 0, file, line, fn, format, args); +} + +static int builtin_kmod(sd_device *dev, int argc, char *argv[], bool test) { + int i; + + if (!ctx) + return 0; + + if (argc < 3 || !streq(argv[1], "load")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: expected: load <module>", argv[0]); + + for (i = 2; argv[i]; i++) + (void) module_load_and_warn(ctx, argv[i], false); + + return 0; +} + +/* called at udev startup and reload */ +static int builtin_kmod_init(void) { + if (ctx) + return 0; + + ctx = kmod_new(NULL, NULL); + if (!ctx) + return -ENOMEM; + + log_debug("Load module index"); + kmod_set_log_fn(ctx, udev_kmod_log, NULL); + kmod_load_resources(ctx); + return 0; +} + +/* called on udev shutdown and reload request */ +static void builtin_kmod_exit(void) { + log_debug("Unload module index"); + ctx = kmod_unref(ctx); +} + +/* called every couple of seconds during event activity; 'true' if config has changed */ +static bool builtin_kmod_validate(void) { + log_debug("Validate module index"); + if (!ctx) + return false; + return (kmod_validate_resources(ctx) != KMOD_RESOURCES_OK); +} + +const UdevBuiltin udev_builtin_kmod = { + .name = "kmod", + .cmd = builtin_kmod, + .init = builtin_kmod_init, + .exit = builtin_kmod_exit, + .validate = builtin_kmod_validate, + .help = "Kernel module loader", + .run_once = false, +}; diff --git a/src/udev/udev-builtin-net_id.c b/src/udev/udev-builtin-net_id.c new file mode 100644 index 0000000..d06a8c7 --- /dev/null +++ b/src/udev/udev-builtin-net_id.c @@ -0,0 +1,961 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * Predictable network interface device names based on: + * - firmware/bios-provided index numbers for on-board devices + * - firmware-provided pci-express hotplug slot index number + * - physical/geographical location of the hardware + * - the interface's MAC address + * + * https://systemd.io/PREDICTABLE_INTERFACE_NAMES + * + * When the code here is changed, man/systemd.net-naming-scheme.xml must be updated too. + */ + +#include <errno.h> +#include <fcntl.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <stdarg.h> +#include <stdlib.h> +#include <unistd.h> +#include <linux/if.h> +#include <linux/pci_regs.h> + +#include "alloc-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "netif-naming-scheme.h" +#include "parse-util.h" +#include "proc-cmdline.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +#define ONBOARD_INDEX_MAX (16*1024-1) + +enum netname_type{ + NET_UNDEF, + NET_PCI, + NET_USB, + NET_BCMA, + NET_VIRTIO, + NET_CCW, + NET_VIO, + NET_PLATFORM, + NET_NETDEVSIM, +}; + +struct netnames { + enum netname_type type; + + uint8_t mac[6]; + bool mac_valid; + + sd_device *pcidev; + char pci_slot[ALTIFNAMSIZ]; + char pci_path[ALTIFNAMSIZ]; + char pci_onboard[ALTIFNAMSIZ]; + const char *pci_onboard_label; + + char usb_ports[ALTIFNAMSIZ]; + char bcma_core[ALTIFNAMSIZ]; + char ccw_busid[ALTIFNAMSIZ]; + char vio_slot[ALTIFNAMSIZ]; + char platform_path[ALTIFNAMSIZ]; + char netdevsim_path[ALTIFNAMSIZ]; +}; + +struct virtfn_info { + sd_device *physfn_pcidev; + char suffix[ALTIFNAMSIZ]; +}; + +/* skip intermediate virtio devices */ +static sd_device *skip_virtio(sd_device *dev) { + sd_device *parent; + + /* there can only ever be one virtio bus per parent device, so we can + * safely ignore any virtio buses. see + * http://lists.linuxfoundation.org/pipermail/virtualization/2015-August/030331.html */ + for (parent = dev; parent; ) { + const char *subsystem; + + if (sd_device_get_subsystem(parent, &subsystem) < 0) + break; + + if (!streq(subsystem, "virtio")) + break; + + if (sd_device_get_parent(parent, &parent) < 0) + return NULL; + } + + return parent; +} + +static int get_virtfn_info(sd_device *dev, struct netnames *names, struct virtfn_info *ret) { + _cleanup_(sd_device_unrefp) sd_device *physfn_pcidev = NULL; + const char *physfn_link_file, *syspath; + _cleanup_free_ char *physfn_pci_syspath = NULL; + _cleanup_free_ char *virtfn_pci_syspath = NULL; + struct dirent *dent; + _cleanup_closedir_ DIR *dir = NULL; + char suffix[ALTIFNAMSIZ]; + int r; + + assert(dev); + assert(names); + assert(ret); + + r = sd_device_get_syspath(names->pcidev, &syspath); + if (r < 0) + return r; + + /* Check if this is a virtual function. */ + physfn_link_file = strjoina(syspath, "/physfn"); + r = chase_symlinks(physfn_link_file, NULL, 0, &physfn_pci_syspath, NULL); + if (r < 0) + return r; + + /* Get physical function's pci device. */ + r = sd_device_new_from_syspath(&physfn_pcidev, physfn_pci_syspath); + if (r < 0) + return r; + + /* Find the virtual function number by finding the right virtfn link. */ + dir = opendir(physfn_pci_syspath); + if (!dir) + return -errno; + + FOREACH_DIRENT_ALL(dent, dir, break) { + _cleanup_free_ char *virtfn_link_file = NULL; + + if (!startswith(dent->d_name, "virtfn")) + continue; + + virtfn_link_file = path_join(physfn_pci_syspath, dent->d_name); + if (!virtfn_link_file) + return -ENOMEM; + + if (chase_symlinks(virtfn_link_file, NULL, 0, &virtfn_pci_syspath, NULL) < 0) + continue; + + if (streq(syspath, virtfn_pci_syspath)) { + if (!snprintf_ok(suffix, sizeof(suffix), "v%s", &dent->d_name[6])) + return -ENOENT; + + break; + } + } + if (isempty(suffix)) + return -ENOENT; + + ret->physfn_pcidev = TAKE_PTR(physfn_pcidev); + strncpy(ret->suffix, suffix, sizeof(ret->suffix)); + + return 0; +} + +/* retrieve on-board index number and label from firmware */ +static int dev_pci_onboard(sd_device *dev, struct netnames *names) { + unsigned long idx, dev_port = 0; + const char *attr, *port_name = NULL; + size_t l; + char *s; + int r; + + /* ACPI _DSM — device specific method for naming a PCI or PCI Express device */ + if (sd_device_get_sysattr_value(names->pcidev, "acpi_index", &attr) < 0) { + /* SMBIOS type 41 — Onboard Devices Extended Information */ + r = sd_device_get_sysattr_value(names->pcidev, "index", &attr); + if (r < 0) + return r; + } + + r = safe_atolu(attr, &idx); + if (r < 0) + return r; + if (idx == 0 && !naming_scheme_has(NAMING_ZERO_ACPI_INDEX)) + return -EINVAL; + + /* Some BIOSes report rubbish indexes that are excessively high (2^24-1 is an index VMware likes to + * report for example). Let's define a cut-off where we don't consider the index reliable anymore. We + * pick some arbitrary cut-off, which is somewhere beyond the realistic number of physical network + * interface a system might have. Ideally the kernel would already filter this crap for us, but it + * doesn't currently. */ + if (idx > ONBOARD_INDEX_MAX) + return -ENOENT; + + /* kernel provided port index for multiple ports on a single PCI function */ + if (sd_device_get_sysattr_value(dev, "dev_port", &attr) >= 0) + dev_port = strtoul(attr, NULL, 10); + + /* kernel provided front panel port name for multiple port PCI device */ + (void) sd_device_get_sysattr_value(dev, "phys_port_name", &port_name); + + s = names->pci_onboard; + l = sizeof(names->pci_onboard); + l = strpcpyf(&s, l, "o%lu", idx); + if (port_name) + l = strpcpyf(&s, l, "n%s", port_name); + else if (dev_port > 0) + l = strpcpyf(&s, l, "d%lu", dev_port); + if (l == 0) + names->pci_onboard[0] = '\0'; + + if (sd_device_get_sysattr_value(names->pcidev, "label", &names->pci_onboard_label) < 0) + names->pci_onboard_label = NULL; + + return 0; +} + +/* read the 256 bytes PCI configuration space to check the multi-function bit */ +static bool is_pci_multifunction(sd_device *dev) { + _cleanup_close_ int fd = -1; + const char *filename, *syspath; + uint8_t config[64]; + + if (sd_device_get_syspath(dev, &syspath) < 0) + return false; + + filename = strjoina(syspath, "/config"); + fd = open(filename, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + if (read(fd, &config, sizeof(config)) != sizeof(config)) + return false; + + /* bit 0-6 header type, bit 7 multi/single function device */ + return config[PCI_HEADER_TYPE] & 0x80; +} + +static bool is_pci_ari_enabled(sd_device *dev) { + const char *a; + + if (sd_device_get_sysattr_value(dev, "ari_enabled", &a) < 0) + return false; + + return streq(a, "1"); +} + +static bool is_pci_bridge(sd_device *dev) { + const char *v, *p; + + if (sd_device_get_sysattr_value(dev, "modalias", &v) < 0) + return false; + + if (!startswith(v, "pci:")) + return false; + + p = strrchr(v, 's'); + if (!p) + return false; + if (p[1] != 'c') + return false; + + /* PCI device subclass 04 corresponds to PCI bridge */ + return strneq(p + 2, "04", 2); +} + +static int dev_pci_slot(sd_device *dev, struct netnames *names) { + unsigned long dev_port = 0; + unsigned domain, bus, slot, func; + int hotplug_slot = -1; + size_t l; + char *s; + const char *sysname, *attr, *port_name = NULL, *syspath; + _cleanup_(sd_device_unrefp) sd_device *pci = NULL; + sd_device *hotplug_slot_dev; + char slots[PATH_MAX]; + _cleanup_closedir_ DIR *dir = NULL; + struct dirent *dent; + int r; + + r = sd_device_get_sysname(names->pcidev, &sysname); + if (r < 0) + return r; + + if (sscanf(sysname, "%x:%x:%x.%u", &domain, &bus, &slot, &func) != 4) + return -ENOENT; + + if (naming_scheme_has(NAMING_NPAR_ARI) && + is_pci_ari_enabled(names->pcidev)) + /* ARI devices support up to 256 functions on a single device ("slot"), and interpret the + * traditional 5-bit slot and 3-bit function number as a single 8-bit function number, + * where the slot makes up the upper 5 bits. */ + func += slot * 8; + + /* kernel provided port index for multiple ports on a single PCI function */ + if (sd_device_get_sysattr_value(dev, "dev_port", &attr) >= 0) { + dev_port = strtoul(attr, NULL, 10); + /* With older kernels IP-over-InfiniBand network interfaces sometimes erroneously + * provide the port number in the 'dev_id' sysfs attribute instead of 'dev_port', + * which thus stays initialized as 0. */ + if (dev_port == 0 && + sd_device_get_sysattr_value(dev, "type", &attr) >= 0) { + unsigned long type; + + type = strtoul(attr, NULL, 10); + if (type == ARPHRD_INFINIBAND && + sd_device_get_sysattr_value(dev, "dev_id", &attr) >= 0) + dev_port = strtoul(attr, NULL, 16); + } + } + + /* kernel provided front panel port name for multi-port PCI device */ + (void) sd_device_get_sysattr_value(dev, "phys_port_name", &port_name); + + /* compose a name based on the raw kernel's PCI bus, slot numbers */ + s = names->pci_path; + l = sizeof(names->pci_path); + if (domain > 0) + l = strpcpyf(&s, l, "P%u", domain); + l = strpcpyf(&s, l, "p%us%u", bus, slot); + if (func > 0 || is_pci_multifunction(names->pcidev)) + l = strpcpyf(&s, l, "f%u", func); + if (port_name) + l = strpcpyf(&s, l, "n%s", port_name); + else if (dev_port > 0) + l = strpcpyf(&s, l, "d%lu", dev_port); + if (l == 0) + names->pci_path[0] = '\0'; + + /* ACPI _SUN — slot user number */ + r = sd_device_new_from_subsystem_sysname(&pci, "subsystem", "pci"); + if (r < 0) + return r; + + r = sd_device_get_syspath(pci, &syspath); + if (r < 0) + return r; + if (!snprintf_ok(slots, sizeof slots, "%s/slots", syspath)) + return -ENAMETOOLONG; + + dir = opendir(slots); + if (!dir) + return -errno; + + hotplug_slot_dev = names->pcidev; + while (hotplug_slot_dev) { + if (sd_device_get_sysname(hotplug_slot_dev, &sysname) < 0) + continue; + + FOREACH_DIRENT_ALL(dent, dir, break) { + int i; + char str[PATH_MAX]; + _cleanup_free_ char *address = NULL; + + if (dot_or_dot_dot(dent->d_name)) + continue; + + r = safe_atoi(dent->d_name, &i); + if (r < 0 || i <= 0) + continue; + + /* match slot address with device by stripping the function */ + if (snprintf_ok(str, sizeof str, "%s/%s/address", slots, dent->d_name) && + read_one_line_file(str, &address) >= 0 && + startswith(sysname, address)) { + hotplug_slot = i; + + /* We found the match between PCI device and slot. However, we won't use the + * slot index if the device is a PCI bridge, because it can have other child + * devices that will try to claim the same index and that would create name + * collision. */ + if (naming_scheme_has(NAMING_BRIDGE_NO_SLOT) && is_pci_bridge(hotplug_slot_dev)) + hotplug_slot = 0; + + break; + } + } + if (hotplug_slot >= 0) + break; + if (sd_device_get_parent_with_subsystem_devtype(hotplug_slot_dev, "pci", NULL, &hotplug_slot_dev) < 0) + break; + rewinddir(dir); + } + + if (hotplug_slot > 0) { + s = names->pci_slot; + l = sizeof(names->pci_slot); + if (domain > 0) + l = strpcpyf(&s, l, "P%d", domain); + l = strpcpyf(&s, l, "s%d", hotplug_slot); + if (func > 0 || is_pci_multifunction(names->pcidev)) + l = strpcpyf(&s, l, "f%d", func); + if (port_name) + l = strpcpyf(&s, l, "n%s", port_name); + else if (dev_port > 0) + l = strpcpyf(&s, l, "d%lu", dev_port); + if (l == 0) + names->pci_slot[0] = '\0'; + } + + return 0; +} + +static int names_vio(sd_device *dev, struct netnames *names) { + sd_device *parent; + unsigned busid, slotid, ethid; + const char *syspath, *subsystem; + int r; + + /* check if our direct parent is a VIO device with no other bus in-between */ + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return r; + + r = sd_device_get_subsystem(parent, &subsystem); + if (r < 0) + return r; + if (!streq("vio", subsystem)) + return -ENOENT; + + /* The devices' $DEVPATH number is tied to (virtual) hardware (slot id + * selected in the HMC), thus this provides a reliable naming (e.g. + * "/devices/vio/30000002/net/eth1"); we ignore the bus number, as + * there should only ever be one bus, and then remove leading zeros. */ + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + if (sscanf(syspath, "/sys/devices/vio/%4x%4x/net/eth%u", &busid, &slotid, ðid) != 3) + return -EINVAL; + + xsprintf(names->vio_slot, "v%u", slotid); + names->type = NET_VIO; + return 0; +} + +#define _PLATFORM_TEST "/sys/devices/platform/vvvvPPPP" +#define _PLATFORM_PATTERN4 "/sys/devices/platform/%4s%4x:%2x/net/eth%u" +#define _PLATFORM_PATTERN3 "/sys/devices/platform/%3s%4x:%2x/net/eth%u" + +static int names_platform(sd_device *dev, struct netnames *names, bool test) { + sd_device *parent; + char vendor[5]; + unsigned model, instance, ethid; + const char *syspath, *pattern, *validchars, *subsystem; + int r; + + /* check if our direct parent is a platform device with no other bus in-between */ + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return r; + + r = sd_device_get_subsystem(parent, &subsystem); + if (r < 0) + return r; + + if (!streq("platform", subsystem)) + return -ENOENT; + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + /* syspath is too short, to have a valid ACPI instance */ + if (strlen(syspath) < sizeof _PLATFORM_TEST) + return -EINVAL; + + /* Vendor ID can be either PNP ID (3 chars A-Z) or ACPI ID (4 chars A-Z and numerals) */ + if (syspath[sizeof _PLATFORM_TEST - 1] == ':') { + pattern = _PLATFORM_PATTERN4; + validchars = UPPERCASE_LETTERS DIGITS; + } else { + pattern = _PLATFORM_PATTERN3; + validchars = UPPERCASE_LETTERS; + } + + /* Platform devices are named after ACPI table match, and instance id + * eg. "/sys/devices/platform/HISI00C2:00"); + * The Vendor (3 or 4 char), followed by hexdecimal model number : instance id. + */ + + DISABLE_WARNING_FORMAT_NONLITERAL; + if (sscanf(syspath, pattern, vendor, &model, &instance, ðid) != 4) + return -EINVAL; + REENABLE_WARNING; + + if (!in_charset(vendor, validchars)) + return -ENOENT; + + ascii_strlower(vendor); + + xsprintf(names->platform_path, "a%s%xi%u", vendor, model, instance); + names->type = NET_PLATFORM; + return 0; +} + +static int names_pci(sd_device *dev, struct netnames *names) { + sd_device *parent; + struct netnames vf_names = {}; + struct virtfn_info vf_info = {}; + const char *subsystem; + int r; + + assert(dev); + assert(names); + + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return r; + /* skip virtio subsystem if present */ + parent = skip_virtio(parent); + + if (!parent) + return -ENOENT; + + /* check if our direct parent is a PCI device with no other bus in-between */ + if (sd_device_get_subsystem(parent, &subsystem) >= 0 && + streq("pci", subsystem)) { + names->type = NET_PCI; + names->pcidev = parent; + } else { + r = sd_device_get_parent_with_subsystem_devtype(dev, "pci", NULL, &names->pcidev); + if (r < 0) + return r; + } + + if (naming_scheme_has(NAMING_SR_IOV_V) && + get_virtfn_info(dev, names, &vf_info) >= 0) { + /* If this is an SR-IOV virtual device, get base name using physical device and add virtfn suffix. */ + vf_names.pcidev = vf_info.physfn_pcidev; + dev_pci_onboard(dev, &vf_names); + dev_pci_slot(dev, &vf_names); + if (vf_names.pci_onboard[0]) + if (strlen(vf_names.pci_onboard) + strlen(vf_info.suffix) < sizeof(names->pci_onboard)) + strscpyl(names->pci_onboard, sizeof(names->pci_onboard), + vf_names.pci_onboard, vf_info.suffix, NULL); + if (vf_names.pci_slot[0]) + if (strlen(vf_names.pci_slot) + strlen(vf_info.suffix) < sizeof(names->pci_slot)) + strscpyl(names->pci_slot, sizeof(names->pci_slot), + vf_names.pci_slot, vf_info.suffix, NULL); + if (vf_names.pci_path[0]) + if (strlen(vf_names.pci_path) + strlen(vf_info.suffix) < sizeof(names->pci_path)) + strscpyl(names->pci_path, sizeof(names->pci_path), + vf_names.pci_path, vf_info.suffix, NULL); + sd_device_unref(vf_info.physfn_pcidev); + } else { + dev_pci_onboard(dev, names); + dev_pci_slot(dev, names); + } + + return 0; +} + +static int names_usb(sd_device *dev, struct netnames *names) { + sd_device *usbdev; + char name[256], *ports, *config, *interf, *s; + const char *sysname; + size_t l; + int r; + + assert(dev); + assert(names); + + r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &usbdev); + if (r < 0) + return r; + + r = sd_device_get_sysname(usbdev, &sysname); + if (r < 0) + return r; + + /* get USB port number chain, configuration, interface */ + strscpy(name, sizeof(name), sysname); + s = strchr(name, '-'); + if (!s) + return -EINVAL; + ports = s+1; + + s = strchr(ports, ':'); + if (!s) + return -EINVAL; + s[0] = '\0'; + config = s+1; + + s = strchr(config, '.'); + if (!s) + return -EINVAL; + s[0] = '\0'; + interf = s+1; + + /* prefix every port number in the chain with "u" */ + s = ports; + while ((s = strchr(s, '.'))) + s[0] = 'u'; + s = names->usb_ports; + l = strpcpyl(&s, sizeof(names->usb_ports), "u", ports, NULL); + + /* append USB config number, suppress the common config == 1 */ + if (!streq(config, "1")) + l = strpcpyl(&s, sizeof(names->usb_ports), "c", config, NULL); + + /* append USB interface number, suppress the interface == 0 */ + if (!streq(interf, "0")) + l = strpcpyl(&s, sizeof(names->usb_ports), "i", interf, NULL); + if (l == 0) + return -ENAMETOOLONG; + + names->type = NET_USB; + return 0; +} + +static int names_bcma(sd_device *dev, struct netnames *names) { + sd_device *bcmadev; + unsigned core; + const char *sysname; + int r; + + assert(dev); + assert(names); + + r = sd_device_get_parent_with_subsystem_devtype(dev, "bcma", NULL, &bcmadev); + if (r < 0) + return r; + + r = sd_device_get_sysname(bcmadev, &sysname); + if (r < 0) + return r; + + /* bus num:core num */ + if (sscanf(sysname, "bcma%*u:%u", &core) != 1) + return -EINVAL; + /* suppress the common core == 0 */ + if (core > 0) + xsprintf(names->bcma_core, "b%u", core); + + names->type = NET_BCMA; + return 0; +} + +static int names_ccw(sd_device *dev, struct netnames *names) { + sd_device *cdev; + const char *bus_id, *subsys; + size_t bus_id_len; + size_t bus_id_start; + int r; + + assert(dev); + assert(names); + + /* Retrieve the associated CCW device */ + r = sd_device_get_parent(dev, &cdev); + if (r < 0) + return r; + + /* skip virtio subsystem if present */ + cdev = skip_virtio(cdev); + if (!cdev) + return -ENOENT; + + r = sd_device_get_subsystem(cdev, &subsys); + if (r < 0) + return r; + + /* Network devices are either single or grouped CCW devices */ + if (!STR_IN_SET(subsys, "ccwgroup", "ccw")) + return -ENOENT; + + /* Retrieve bus-ID of the CCW device. The bus-ID uniquely + * identifies the network device on the Linux on System z channel + * subsystem. Note that the bus-ID contains lowercase characters. + */ + r = sd_device_get_sysname(cdev, &bus_id); + if (r < 0) + return r; + + /* Check the length of the bus-ID. Rely on the fact that the kernel provides a correct bus-ID; + * alternatively, improve this check and parse and verify each bus-ID part... + */ + bus_id_len = strlen(bus_id); + if (!IN_SET(bus_id_len, 8, 9)) + return -EINVAL; + + /* Strip leading zeros from the bus id for aesthetic purposes. This + * keeps the ccw names stable, yet much shorter in general case of + * bus_id 0.0.0600 -> 600. This is similar to e.g. how PCI domain is + * not prepended when it is zero. Preserve the last 0 for 0.0.0000. + */ + bus_id_start = strspn(bus_id, ".0"); + bus_id += bus_id_start < bus_id_len ? bus_id_start : bus_id_len - 1; + + /* Store the CCW bus-ID for use as network device name */ + if (snprintf_ok(names->ccw_busid, sizeof(names->ccw_busid), "c%s", bus_id)) + names->type = NET_CCW; + + return 0; +} + +static int names_mac(sd_device *dev, struct netnames *names) { + const char *s; + unsigned long i; + unsigned a1, a2, a3, a4, a5, a6; + int r; + + /* Some kinds of devices tend to have hardware addresses + * that are impossible to use in an iface name. + */ + r = sd_device_get_sysattr_value(dev, "type", &s); + if (r < 0) + return r; + + i = strtoul(s, NULL, 0); + switch (i) { + /* The persistent part of a hardware address of an InfiniBand NIC + * is 8 bytes long. We cannot fit this much in an iface name. + */ + case ARPHRD_INFINIBAND: + return -EINVAL; + default: + break; + } + + /* check for NET_ADDR_PERM, skip random MAC addresses */ + r = sd_device_get_sysattr_value(dev, "addr_assign_type", &s); + if (r < 0) + return r; + i = strtoul(s, NULL, 0); + if (i != 0) + return 0; + + r = sd_device_get_sysattr_value(dev, "address", &s); + if (r < 0) + return r; + if (sscanf(s, "%x:%x:%x:%x:%x:%x", &a1, &a2, &a3, &a4, &a5, &a6) != 6) + return -EINVAL; + + /* skip empty MAC addresses */ + if (a1 + a2 + a3 + a4 + a5 + a6 == 0) + return -EINVAL; + + names->mac[0] = a1; + names->mac[1] = a2; + names->mac[2] = a3; + names->mac[3] = a4; + names->mac[4] = a5; + names->mac[5] = a6; + names->mac_valid = true; + return 0; +} + +static int names_netdevsim(sd_device *dev, struct netnames *names) { + sd_device *netdevsimdev; + const char *sysname; + unsigned addr; + const char *port_name = NULL; + int r; + bool ok; + + if (!naming_scheme_has(NAMING_NETDEVSIM)) + return 0; + + assert(dev); + assert(names); + + r = sd_device_get_parent_with_subsystem_devtype(dev, "netdevsim", NULL, &netdevsimdev); + if (r < 0) + return r; + r = sd_device_get_sysname(netdevsimdev, &sysname); + if (r < 0) + return r; + + if (sscanf(sysname, "netdevsim%u", &addr) != 1) + return -EINVAL; + + r = sd_device_get_sysattr_value(dev, "phys_port_name", &port_name); + if (r < 0) + return r; + + ok = snprintf_ok(names->netdevsim_path, sizeof(names->netdevsim_path), "i%un%s", addr, port_name); + if (!ok) + return -ENOBUFS; + + names->type = NET_NETDEVSIM; + + return 0; +} + +/* IEEE Organizationally Unique Identifier vendor string */ +static int ieee_oui(sd_device *dev, struct netnames *names, bool test) { + char str[32]; + + if (!names->mac_valid) + return -ENOENT; + /* skip commonly misused 00:00:00 (Xerox) prefix */ + if (memcmp(names->mac, "\0\0\0", 3) == 0) + return -EINVAL; + xsprintf(str, "OUI:%02X%02X%02X%02X%02X%02X", names->mac[0], + names->mac[1], names->mac[2], names->mac[3], names->mac[4], + names->mac[5]); + udev_builtin_hwdb_lookup(dev, NULL, str, NULL, test); + return 0; +} + +static int builtin_net_id(sd_device *dev, int argc, char *argv[], bool test) { + const char *s, *p, *devtype, *prefix = "en"; + struct netnames names = {}; + unsigned long i; + int r; + + /* handle only ARPHRD_ETHER, ARPHRD_SLIP and ARPHRD_INFINIBAND devices */ + r = sd_device_get_sysattr_value(dev, "type", &s); + if (r < 0) + return r; + + i = strtoul(s, NULL, 0); + switch (i) { + case ARPHRD_ETHER: + prefix = "en"; + break; + case ARPHRD_INFINIBAND: + if (naming_scheme_has(NAMING_INFINIBAND)) + prefix = "ib"; + else + return 0; + break; + case ARPHRD_SLIP: + prefix = "sl"; + break; + default: + return 0; + } + + /* skip stacked devices, like VLANs, ... */ + r = sd_device_get_sysattr_value(dev, "ifindex", &s); + if (r < 0) + return r; + r = sd_device_get_sysattr_value(dev, "iflink", &p); + if (r < 0) + return r; + if (!streq(s, p)) + return 0; + + if (sd_device_get_devtype(dev, &devtype) >= 0) { + if (streq("wlan", devtype)) + prefix = "wl"; + else if (streq("wwan", devtype)) + prefix = "ww"; + } + + udev_builtin_add_property(dev, test, "ID_NET_NAMING_SCHEME", naming_scheme()->name); + + r = names_mac(dev, &names); + if (r >= 0 && names.mac_valid) { + char str[ALTIFNAMSIZ]; + + xsprintf(str, "%sx%02x%02x%02x%02x%02x%02x", prefix, + names.mac[0], names.mac[1], names.mac[2], + names.mac[3], names.mac[4], names.mac[5]); + udev_builtin_add_property(dev, test, "ID_NET_NAME_MAC", str); + + ieee_oui(dev, &names, test); + } + + /* get path names for Linux on System z network devices */ + if (names_ccw(dev, &names) >= 0 && names.type == NET_CCW) { + char str[ALTIFNAMSIZ]; + + if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.ccw_busid)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + return 0; + } + + /* get ibmveth/ibmvnic slot-based names. */ + if (names_vio(dev, &names) >= 0 && names.type == NET_VIO) { + char str[ALTIFNAMSIZ]; + + if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.vio_slot)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + return 0; + } + + /* get ACPI path names for ARM64 platform devices */ + if (names_platform(dev, &names, test) >= 0 && names.type == NET_PLATFORM) { + char str[ALTIFNAMSIZ]; + + if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.platform_path)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + return 0; + } + + /* get netdevsim path names */ + if (names_netdevsim(dev, &names) >= 0 && names.type == NET_NETDEVSIM) { + char str[ALTIFNAMSIZ]; + + if (snprintf_ok(str, sizeof str, "%s%s", prefix, names.netdevsim_path)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + + return 0; + } + + /* get PCI based path names, we compose only PCI based paths */ + if (names_pci(dev, &names) < 0) + return 0; + + /* plain PCI device */ + if (names.type == NET_PCI) { + char str[ALTIFNAMSIZ]; + + if (names.pci_onboard[0] && + snprintf_ok(str, sizeof str, "%s%s", prefix, names.pci_onboard)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_ONBOARD", str); + + if (names.pci_onboard_label && + snprintf_ok(str, sizeof str, "%s%s", + naming_scheme_has(NAMING_LABEL_NOPREFIX) ? "" : prefix, + names.pci_onboard_label)) + udev_builtin_add_property(dev, test, "ID_NET_LABEL_ONBOARD", str); + + if (names.pci_path[0] && + snprintf_ok(str, sizeof str, "%s%s", prefix, names.pci_path)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + + if (names.pci_slot[0] && + snprintf_ok(str, sizeof str, "%s%s", prefix, names.pci_slot)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + return 0; + } + + /* USB device */ + if (names_usb(dev, &names) >= 0 && names.type == NET_USB) { + char str[ALTIFNAMSIZ]; + + if (names.pci_path[0] && + snprintf_ok(str, sizeof str, "%s%s%s", prefix, names.pci_path, names.usb_ports)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + + if (names.pci_slot[0] && + snprintf_ok(str, sizeof str, "%s%s%s", prefix, names.pci_slot, names.usb_ports)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + return 0; + } + + /* Broadcom bus */ + if (names_bcma(dev, &names) >= 0 && names.type == NET_BCMA) { + char str[ALTIFNAMSIZ]; + + if (names.pci_path[0] && + snprintf_ok(str, sizeof str, "%s%s%s", prefix, names.pci_path, names.bcma_core)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str); + + if (names.pci_slot[0] && + snprintf(str, sizeof str, "%s%s%s", prefix, names.pci_slot, names.bcma_core)) + udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str); + return 0; + } + + return 0; +} + +const UdevBuiltin udev_builtin_net_id = { + .name = "net_id", + .cmd = builtin_net_id, + .help = "Network device properties", +}; diff --git a/src/udev/udev-builtin-net_setup_link.c b/src/udev/udev-builtin-net_setup_link.c new file mode 100644 index 0000000..cb12b94 --- /dev/null +++ b/src/udev/udev-builtin-net_setup_link.c @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-util.h" +#include "alloc-util.h" +#include "link-config.h" +#include "log.h" +#include "string-util.h" +#include "udev-builtin.h" + +static link_config_ctx *ctx = NULL; + +static int builtin_net_setup_link(sd_device *dev, int argc, char **argv, bool test) { + _cleanup_free_ char *driver = NULL; + const char *name = NULL; + link_config *link; + int r; + + if (argc > 1) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + r = link_get_driver(ctx, dev, &driver); + if (r < 0) + log_device_full_errno(dev, r == -EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, + r, "Failed to query device driver: %m"); + else + udev_builtin_add_property(dev, test, "ID_NET_DRIVER", driver); + + r = link_config_get(ctx, dev, &link); + if (r < 0) { + if (r == -ENOENT) + return log_device_debug_errno(dev, r, "No matching link configuration found."); + + return log_device_error_errno(dev, r, "Failed to get link config: %m"); + } + + r = link_config_apply(ctx, link, dev, &name); + if (r < 0) + log_device_warning_errno(dev, r, "Could not apply link config, ignoring: %m"); + + udev_builtin_add_property(dev, test, "ID_NET_LINK_FILE", link->filename); + + if (name) + udev_builtin_add_property(dev, test, "ID_NET_NAME", name); + + return 0; +} + +static int builtin_net_setup_link_init(void) { + int r; + + if (ctx) + return 0; + + r = link_config_ctx_new(&ctx); + if (r < 0) + return r; + + r = link_config_load(ctx); + if (r < 0) + return r; + + log_debug("Created link configuration context."); + return 0; +} + +static void builtin_net_setup_link_exit(void) { + link_config_ctx_free(ctx); + ctx = NULL; + log_debug("Unloaded link configuration context."); +} + +static bool builtin_net_setup_link_validate(void) { + log_debug("Check if link configuration needs reloading."); + if (!ctx) + return false; + + return link_config_should_reload(ctx); +} + +const UdevBuiltin udev_builtin_net_setup_link = { + .name = "net_setup_link", + .cmd = builtin_net_setup_link, + .init = builtin_net_setup_link_init, + .exit = builtin_net_setup_link_exit, + .validate = builtin_net_setup_link_validate, + .help = "Configure network link", + .run_once = false, +}; diff --git a/src/udev/udev-builtin-path_id.c b/src/udev/udev-builtin-path_id.c new file mode 100644 index 0000000..0da59e2 --- /dev/null +++ b/src/udev/udev-builtin-path_id.c @@ -0,0 +1,732 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * compose persistent device path + * + * Logic based on Hannes Reinecke's shell script. + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "libudev-util.h" +#include "string-util.h" +#include "strv.h" +#include "sysexits.h" +#include "udev-builtin.h" + +_printf_(2,3) +static void path_prepend(char **path, const char *fmt, ...) { + va_list va; + _cleanup_free_ char *pre = NULL; + int r; + + va_start(va, fmt); + r = vasprintf(&pre, fmt, va); + va_end(va); + if (r < 0) { + log_oom(); + exit(EX_OSERR); + } + + if (*path) { + char *new; + + new = strjoin(pre, "-", *path); + if (!new) { + log_oom(); + exit(EX_OSERR); + } + + free_and_replace(*path, new); + } else + *path = TAKE_PTR(pre); +} + +/* +** Linux only supports 32 bit luns. +** See drivers/scsi/scsi_scan.c::scsilun_to_int() for more details. +*/ +static int format_lun_number(sd_device *dev, char **path) { + const char *sysnum; + unsigned long lun; + int r; + + r = sd_device_get_sysnum(dev, &sysnum); + if (r < 0) + return r; + if (!sysnum) + return -ENOENT; + + lun = strtoul(sysnum, NULL, 10); + if (lun < 256) + /* address method 0, peripheral device addressing with bus id of zero */ + path_prepend(path, "lun-%lu", lun); + else + /* handle all other lun addressing methods by using a variant of the original lun format */ + path_prepend(path, "lun-0x%04lx%04lx00000000", lun & 0xffff, (lun >> 16) & 0xffff); + + return 0; +} + +static sd_device *skip_subsystem(sd_device *dev, const char *subsys) { + sd_device *parent; + + assert(dev); + assert(subsys); + + for (parent = dev; ; ) { + const char *subsystem; + + if (sd_device_get_subsystem(parent, &subsystem) < 0) + break; + + if (!streq(subsystem, subsys)) + break; + + dev = parent; + if (sd_device_get_parent(dev, &parent) < 0) + break; + } + + return dev; +} + +static sd_device *handle_scsi_fibre_channel(sd_device *parent, char **path) { + sd_device *targetdev; + _cleanup_(sd_device_unrefp) sd_device *fcdev = NULL; + const char *port, *sysname; + _cleanup_free_ char *lun = NULL; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) + return NULL; + if (sd_device_get_sysname(targetdev, &sysname) < 0) + return NULL; + if (sd_device_new_from_subsystem_sysname(&fcdev, "fc_transport", sysname) < 0) + return NULL; + if (sd_device_get_sysattr_value(fcdev, "port_name", &port) < 0) + return NULL; + + format_lun_number(parent, &lun); + path_prepend(path, "fc-%s-%s", port, lun); + return parent; +} + +static sd_device *handle_scsi_sas_wide_port(sd_device *parent, char **path) { + sd_device *targetdev, *target_parent; + _cleanup_(sd_device_unrefp) sd_device *sasdev = NULL; + const char *sas_address, *sysname; + _cleanup_free_ char *lun = NULL; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) + return NULL; + if (sd_device_get_parent(targetdev, &target_parent) < 0) + return NULL; + if (sd_device_get_sysname(target_parent, &sysname) < 0) + return NULL; + if (sd_device_new_from_subsystem_sysname(&sasdev, "sas_device", sysname) < 0) + return NULL; + if (sd_device_get_sysattr_value(sasdev, "sas_address", &sas_address) < 0) + return NULL; + + format_lun_number(parent, &lun); + path_prepend(path, "sas-%s-%s", sas_address, lun); + return parent; +} + +static sd_device *handle_scsi_sas(sd_device *parent, char **path) { + sd_device *targetdev, *target_parent, *port, *expander; + _cleanup_(sd_device_unrefp) sd_device *target_sasdev = NULL, *expander_sasdev = NULL, *port_sasdev = NULL; + const char *sas_address = NULL; + const char *phy_id; + const char *phy_count, *sysname; + _cleanup_free_ char *lun = NULL; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0) + return NULL; + if (sd_device_get_parent(targetdev, &target_parent) < 0) + return NULL; + if (sd_device_get_sysname(target_parent, &sysname) < 0) + return NULL; + /* Get sas device */ + if (sd_device_new_from_subsystem_sysname(&target_sasdev, "sas_device", sysname) < 0) + return NULL; + /* The next parent is sas port */ + if (sd_device_get_parent(target_parent, &port) < 0) + return NULL; + if (sd_device_get_sysname(port, &sysname) < 0) + return NULL; + /* Get port device */ + if (sd_device_new_from_subsystem_sysname(&port_sasdev, "sas_port", sysname) < 0) + return NULL; + if (sd_device_get_sysattr_value(port_sasdev, "num_phys", &phy_count) < 0) + return NULL; + + /* Check if we are simple disk */ + if (strncmp(phy_count, "1", 2) != 0) + return handle_scsi_sas_wide_port(parent, path); + + /* Get connected phy */ + if (sd_device_get_sysattr_value(target_sasdev, "phy_identifier", &phy_id) < 0) + return NULL; + + /* The port's parent is either hba or expander */ + if (sd_device_get_parent(port, &expander) < 0) + return NULL; + + if (sd_device_get_sysname(expander, &sysname) < 0) + return NULL; + /* Get expander device */ + if (sd_device_new_from_subsystem_sysname(&expander_sasdev, "sas_device", sysname) >= 0) { + /* Get expander's address */ + if (sd_device_get_sysattr_value(expander_sasdev, "sas_address", &sas_address) < 0) + return NULL; + } + + format_lun_number(parent, &lun); + if (sas_address) + path_prepend(path, "sas-exp%s-phy%s-%s", sas_address, phy_id, lun); + else + path_prepend(path, "sas-phy%s-%s", phy_id, lun); + + return parent; +} + +static sd_device *handle_scsi_iscsi(sd_device *parent, char **path) { + sd_device *transportdev; + _cleanup_(sd_device_unrefp) sd_device *sessiondev = NULL, *conndev = NULL; + const char *target, *connname, *addr, *port; + _cleanup_free_ char *lun = NULL; + const char *sysname, *sysnum; + + assert(parent); + assert(path); + + /* find iscsi session */ + for (transportdev = parent; ; ) { + + if (sd_device_get_parent(transportdev, &transportdev) < 0) + return NULL; + if (sd_device_get_sysname(transportdev, &sysname) < 0) + return NULL; + if (startswith(sysname, "session")) + break; + } + + /* find iscsi session device */ + if (sd_device_new_from_subsystem_sysname(&sessiondev, "iscsi_session", sysname) < 0) + return NULL; + + if (sd_device_get_sysattr_value(sessiondev, "targetname", &target) < 0) + return NULL; + + if (sd_device_get_sysnum(transportdev, &sysnum) < 0 || !sysnum) + return NULL; + connname = strjoina("connection", sysnum, ":0"); + if (sd_device_new_from_subsystem_sysname(&conndev, "iscsi_connection", connname) < 0) + return NULL; + + if (sd_device_get_sysattr_value(conndev, "persistent_address", &addr) < 0) + return NULL; + if (sd_device_get_sysattr_value(conndev, "persistent_port", &port) < 0) + return NULL; + + format_lun_number(parent, &lun); + path_prepend(path, "ip-%s:%s-iscsi-%s-%s", addr, port, target, lun); + return parent; +} + +static sd_device *handle_scsi_ata(sd_device *parent, char **path, char **compat_path) { + sd_device *targetdev, *target_parent; + _cleanup_(sd_device_unrefp) sd_device *atadev = NULL; + const char *port_no, *sysname, *name; + unsigned host, bus, target, lun; + + assert(parent); + assert(path); + + if (sd_device_get_sysname(parent, &name) < 0) + return NULL; + if (sscanf(name, "%u:%u:%u:%u", &host, &bus, &target, &lun) != 4) + return NULL; + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &targetdev) < 0) + return NULL; + + if (sd_device_get_parent(targetdev, &target_parent) < 0) + return NULL; + + if (sd_device_get_sysname(target_parent, &sysname) < 0) + return NULL; + if (sd_device_new_from_subsystem_sysname(&atadev, "ata_port", sysname) < 0) + return NULL; + + if (sd_device_get_sysattr_value(atadev, "port_no", &port_no) < 0) + return NULL; + + if (bus != 0) + /* Devices behind port multiplier have a bus != 0*/ + path_prepend(path, "ata-%s.%u.0", port_no, bus); + else + /* Master/slave are distinguished by target id */ + path_prepend(path, "ata-%s.%u", port_no, target); + + /* old compatible persistent link for ATA devices */ + if (compat_path) + path_prepend(compat_path, "ata-%s", port_no); + + return parent; +} + +static sd_device *handle_scsi_default(sd_device *parent, char **path) { + sd_device *hostdev; + int host, bus, target, lun; + const char *name, *base, *pos; + _cleanup_closedir_ DIR *dir = NULL; + struct dirent *dent; + int basenum = -1; + + assert(parent); + assert(path); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0) + return NULL; + + if (sd_device_get_sysname(parent, &name) < 0) + return NULL; + if (sscanf(name, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4) + return NULL; + + /* + * Rebase host offset to get the local relative number + * + * Note: This is by definition racy, unreliable and too simple. + * Please do not copy this model anywhere. It's just a left-over + * from the time we had no idea how things should look like in + * the end. + * + * Making assumptions about a global in-kernel counter and use + * that to calculate a local offset is a very broken concept. It + * can only work as long as things are in strict order. + * + * The kernel needs to export the instance/port number of a + * controller directly, without the need for rebase magic like + * this. Manual driver unbind/bind, parallel hotplug/unplug will + * get into the way of this "I hope it works" logic. + */ + + if (sd_device_get_syspath(hostdev, &base) < 0) + return NULL; + pos = strrchr(base, '/'); + if (!pos) + return NULL; + + base = strndupa(base, pos - base); + dir = opendir(base); + if (!dir) + return NULL; + + FOREACH_DIRENT_ALL(dent, dir, break) { + char *rest; + int i; + + if (dent->d_name[0] == '.') + continue; + if (!IN_SET(dent->d_type, DT_DIR, DT_LNK)) + continue; + if (!startswith(dent->d_name, "host")) + continue; + i = strtoul(&dent->d_name[4], &rest, 10); + if (rest[0] != '\0') + continue; + /* + * find the smallest number; the host really needs to export its + * own instance number per parent device; relying on the global host + * enumeration and plainly rebasing the numbers sounds unreliable + */ + if (basenum == -1 || i < basenum) + basenum = i; + } + if (basenum == -1) + return hostdev; + host -= basenum; + + path_prepend(path, "scsi-%u:%u:%u:%u", host, bus, target, lun); + return hostdev; +} + +static sd_device *handle_scsi_hyperv(sd_device *parent, char **path, size_t guid_str_len) { + sd_device *hostdev; + sd_device *vmbusdev; + const char *guid_str; + _cleanup_free_ char *lun = NULL; + char guid[39]; + size_t i, k; + + assert(parent); + assert(path); + assert(guid_str_len < sizeof(guid)); + + if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0) + return NULL; + + if (sd_device_get_parent(hostdev, &vmbusdev) < 0) + return NULL; + + if (sd_device_get_sysattr_value(vmbusdev, "device_id", &guid_str) < 0) + return NULL; + + if (strlen(guid_str) < guid_str_len || guid_str[0] != '{' || guid_str[guid_str_len-1] != '}') + return NULL; + + for (i = 1, k = 0; i < guid_str_len-1; i++) { + if (guid_str[i] == '-') + continue; + guid[k++] = guid_str[i]; + } + guid[k] = '\0'; + + format_lun_number(parent, &lun); + path_prepend(path, "vmbus-%s-%s", guid, lun); + return parent; +} + +static sd_device *handle_scsi(sd_device *parent, char **path, char **compat_path, bool *supported_parent) { + const char *devtype, *id, *name; + + if (sd_device_get_devtype(parent, &devtype) < 0 || + !streq(devtype, "scsi_device")) + return parent; + + /* firewire */ + if (sd_device_get_sysattr_value(parent, "ieee1394_id", &id) >= 0) { + path_prepend(path, "ieee1394-0x%s", id); + *supported_parent = true; + return skip_subsystem(parent, "scsi"); + } + + /* scsi sysfs does not have a "subsystem" for the transport */ + if (sd_device_get_syspath(parent, &name) < 0) + return NULL; + + if (strstr(name, "/rport-")) { + *supported_parent = true; + return handle_scsi_fibre_channel(parent, path); + } + + if (strstr(name, "/end_device-")) { + *supported_parent = true; + return handle_scsi_sas(parent, path); + } + + if (strstr(name, "/session")) { + *supported_parent = true; + return handle_scsi_iscsi(parent, path); + } + + if (strstr(name, "/ata")) + return handle_scsi_ata(parent, path, compat_path); + + if (strstr(name, "/vmbus_")) + return handle_scsi_hyperv(parent, path, 37); + else if (strstr(name, "/VMBUS")) + return handle_scsi_hyperv(parent, path, 38); + + return handle_scsi_default(parent, path); +} + +static sd_device *handle_cciss(sd_device *parent, char **path) { + const char *str; + unsigned controller, disk; + + if (sd_device_get_sysname(parent, &str) < 0) + return NULL; + if (sscanf(str, "c%ud%u%*s", &controller, &disk) != 2) + return NULL; + + path_prepend(path, "cciss-disk%u", disk); + return skip_subsystem(parent, "cciss"); +} + +static void handle_scsi_tape(sd_device *dev, char **path) { + const char *name; + + /* must be the last device in the syspath */ + if (*path) + return; + + if (sd_device_get_sysname(dev, &name) < 0) + return; + + if (startswith(name, "nst") && strchr("lma", name[3])) + path_prepend(path, "nst%c", name[3]); + else if (startswith(name, "st") && strchr("lma", name[2])) + path_prepend(path, "st%c", name[2]); +} + +static sd_device *handle_usb(sd_device *parent, char **path) { + const char *devtype, *str, *port; + + if (sd_device_get_devtype(parent, &devtype) < 0) + return parent; + if (!STR_IN_SET(devtype, "usb_interface", "usb_device")) + return parent; + + if (sd_device_get_sysname(parent, &str) < 0) + return parent; + port = strchr(str, '-'); + if (!port) + return parent; + port++; + + path_prepend(path, "usb-0:%s", port); + return skip_subsystem(parent, "usb"); +} + +static sd_device *handle_bcma(sd_device *parent, char **path) { + const char *sysname; + unsigned core; + + if (sd_device_get_sysname(parent, &sysname) < 0) + return NULL; + if (sscanf(sysname, "bcma%*u:%u", &core) != 1) + return NULL; + + path_prepend(path, "bcma-%u", core); + return parent; +} + +/* Handle devices of AP bus in System z platform. */ +static sd_device *handle_ap(sd_device *parent, char **path) { + const char *type, *func; + + assert(parent); + assert(path); + + if (sd_device_get_sysattr_value(parent, "type", &type) >= 0 && + sd_device_get_sysattr_value(parent, "ap_functions", &func) >= 0) + path_prepend(path, "ap-%s-%s", type, func); + else { + const char *sysname; + + if (sd_device_get_sysname(parent, &sysname) >= 0) + path_prepend(path, "ap-%s", sysname); + } + + return skip_subsystem(parent, "ap"); +} + +static int builtin_path_id(sd_device *dev, int argc, char *argv[], bool test) { + sd_device *parent; + _cleanup_free_ char *path = NULL; + _cleanup_free_ char *compat_path = NULL; + bool supported_transport = false; + bool supported_parent = false; + const char *subsystem; + + assert(dev); + + /* walk up the chain of devices and compose path */ + parent = dev; + while (parent) { + const char *subsys, *sysname; + + if (sd_device_get_subsystem(parent, &subsys) < 0 || + sd_device_get_sysname(parent, &sysname) < 0) { + ; + } else if (streq(subsys, "scsi_tape")) { + handle_scsi_tape(parent, &path); + } else if (streq(subsys, "scsi")) { + parent = handle_scsi(parent, &path, &compat_path, &supported_parent); + supported_transport = true; + } else if (streq(subsys, "cciss")) { + parent = handle_cciss(parent, &path); + supported_transport = true; + } else if (streq(subsys, "usb")) { + parent = handle_usb(parent, &path); + supported_transport = true; + } else if (streq(subsys, "bcma")) { + parent = handle_bcma(parent, &path); + supported_transport = true; + } else if (streq(subsys, "serio")) { + const char *sysnum; + + if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) { + path_prepend(&path, "serio-%s", sysnum); + parent = skip_subsystem(parent, "serio"); + } + } else if (streq(subsys, "pci")) { + path_prepend(&path, "pci-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "pci-%s", sysname); + parent = skip_subsystem(parent, "pci"); + supported_parent = true; + } else if (streq(subsys, "platform")) { + path_prepend(&path, "platform-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "platform-%s", sysname); + parent = skip_subsystem(parent, "platform"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "acpi")) { + path_prepend(&path, "acpi-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "acpi-%s", sysname); + parent = skip_subsystem(parent, "acpi"); + supported_parent = true; + } else if (streq(subsys, "xen")) { + path_prepend(&path, "xen-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "xen-%s", sysname); + parent = skip_subsystem(parent, "xen"); + supported_parent = true; + } else if (streq(subsys, "virtio")) { + parent = skip_subsystem(parent, "virtio"); + supported_transport = true; + } else if (streq(subsys, "scm")) { + path_prepend(&path, "scm-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "scm-%s", sysname); + parent = skip_subsystem(parent, "scm"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ccw")) { + path_prepend(&path, "ccw-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "ccw-%s", sysname); + parent = skip_subsystem(parent, "ccw"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ccwgroup")) { + path_prepend(&path, "ccwgroup-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "ccwgroup-%s", sysname); + parent = skip_subsystem(parent, "ccwgroup"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ap")) { + parent = handle_ap(parent, &path); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "iucv")) { + path_prepend(&path, "iucv-%s", sysname); + if (compat_path) + path_prepend(&compat_path, "iucv-%s", sysname); + parent = skip_subsystem(parent, "iucv"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "nvme")) { + const char *nsid; + + if (sd_device_get_sysattr_value(dev, "nsid", &nsid) >= 0) { + path_prepend(&path, "nvme-%s", nsid); + if (compat_path) + path_prepend(&compat_path, "nvme-%s", nsid); + parent = skip_subsystem(parent, "nvme"); + supported_parent = true; + supported_transport = true; + } + } else if (streq(subsys, "spi")) { + const char *sysnum; + + if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) { + path_prepend(&path, "cs-%s", sysnum); + parent = skip_subsystem(parent, "spi"); + } + } + + if (!parent) + break; + if (sd_device_get_parent(parent, &parent) < 0) + break; + } + + if (!path) + return -ENOENT; + + /* + * Do not return devices with an unknown parent device type. They + * might produce conflicting IDs if the parent does not provide a + * unique and predictable name. + */ + if (!supported_parent) + return -ENOENT; + + /* + * Do not return block devices without a well-known transport. Some + * devices do not expose their buses and do not provide a unique + * and predictable name that way. + */ + if (sd_device_get_subsystem(dev, &subsystem) >= 0 && + streq(subsystem, "block") && + !supported_transport) + return -ENOENT; + + { + char tag[UTIL_NAME_SIZE]; + size_t i; + const char *p; + + /* compose valid udev tag name */ + for (p = path, i = 0; *p; p++) { + if ((*p >= '0' && *p <= '9') || + (*p >= 'A' && *p <= 'Z') || + (*p >= 'a' && *p <= 'z') || + *p == '-') { + tag[i++] = *p; + continue; + } + + /* skip all leading '_' */ + if (i == 0) + continue; + + /* avoid second '_' */ + if (tag[i-1] == '_') + continue; + + tag[i++] = '_'; + } + /* strip trailing '_' */ + while (i > 0 && tag[i-1] == '_') + i--; + tag[i] = '\0'; + + udev_builtin_add_property(dev, test, "ID_PATH", path); + udev_builtin_add_property(dev, test, "ID_PATH_TAG", tag); + } + + /* + * Compatible link generation for ATA devices + * we assign compat_link to the env variable + * ID_PATH_ATA_COMPAT + */ + if (compat_path) + udev_builtin_add_property(dev, test, "ID_PATH_ATA_COMPAT", compat_path); + + return 0; +} + +const UdevBuiltin udev_builtin_path_id = { + .name = "path_id", + .cmd = builtin_path_id, + .help = "Compose persistent device path", + .run_once = true, +}; diff --git a/src/udev/udev-builtin-uaccess.c b/src/udev/udev-builtin-uaccess.c new file mode 100644 index 0000000..cfdf130 --- /dev/null +++ b/src/udev/udev-builtin-uaccess.c @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * manage device node user ACL + */ + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> + +#include "sd-login.h" + +#include "device-util.h" +#include "login-util.h" +#include "logind-acl.h" +#include "log.h" +#include "udev-builtin.h" + +static int builtin_uaccess(sd_device *dev, int argc, char *argv[], bool test) { + const char *path = NULL, *seat; + bool changed_acl = false; + uid_t uid; + int r; + + umask(0022); + + /* don't muck around with ACLs when the system is not running systemd */ + if (!logind_running()) + return 0; + + r = sd_device_get_devname(dev, &path); + if (r < 0) { + log_device_error_errno(dev, r, "Failed to get device name: %m"); + goto finish; + } + + if (sd_device_get_property_value(dev, "ID_SEAT", &seat) < 0) + seat = "seat0"; + + r = sd_seat_get_active(seat, NULL, &uid); + if (r < 0) { + if (IN_SET(r, -ENXIO, -ENODATA)) + /* No active session on this seat */ + r = 0; + else + log_device_error_errno(dev, r, "Failed to determine active user on seat %s: %m", seat); + + goto finish; + } + + r = devnode_acl(path, true, false, 0, true, uid); + if (r < 0) { + log_device_full_errno(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to apply ACL: %m"); + goto finish; + } + + changed_acl = true; + r = 0; + +finish: + if (path && !changed_acl) { + int k; + + /* Better be safe than sorry and reset ACL */ + k = devnode_acl(path, true, false, 0, false, 0); + if (k < 0) { + log_device_full_errno(dev, k == -ENOENT ? LOG_DEBUG : LOG_ERR, k, "Failed to apply ACL: %m"); + if (r >= 0) + r = k; + } + } + + return r; +} + +const UdevBuiltin udev_builtin_uaccess = { + .name = "uaccess", + .cmd = builtin_uaccess, + .help = "Manage device node user ACL", +}; diff --git a/src/udev/udev-builtin-usb_id.c b/src/udev/udev-builtin-usb_id.c new file mode 100644 index 0000000..fa554e7 --- /dev/null +++ b/src/udev/udev-builtin-usb_id.c @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * USB device properties and persistent device path + * + * Copyright (c) 2005 SUSE Linux Products GmbH, Germany + * Author: Hannes Reinecke <hare@suse.de> + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "device-util.h" +#include "fd-util.h" +#include "libudev-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" + +static void set_usb_iftype(char *to, int if_class_num, size_t len) { + const char *type = "generic"; + + switch (if_class_num) { + case 1: + type = "audio"; + break; + case 2: /* CDC-Control */ + break; + case 3: + type = "hid"; + break; + case 5: /* Physical */ + break; + case 6: + type = "media"; + break; + case 7: + type = "printer"; + break; + case 8: + type = "storage"; + break; + case 9: + type = "hub"; + break; + case 0x0a: /* CDC-Data */ + break; + case 0x0b: /* Chip/Smart Card */ + break; + case 0x0d: /* Content Security */ + break; + case 0x0e: + type = "video"; + break; + case 0xdc: /* Diagnostic Device */ + break; + case 0xe0: /* Wireless Controller */ + break; + case 0xfe: /* Application-specific */ + break; + case 0xff: /* Vendor-specific */ + break; + default: + break; + } + strncpy(to, type, len); + to[len-1] = '\0'; +} + +static int set_usb_mass_storage_ifsubtype(char *to, const char *from, size_t len) { + int type_num = 0; + char *eptr; + const char *type = "generic"; + + type_num = strtoul(from, &eptr, 0); + if (eptr != from) { + switch (type_num) { + case 1: /* RBC devices */ + type = "rbc"; + break; + case 2: + type = "atapi"; + break; + case 3: + type = "tape"; + break; + case 4: /* UFI */ + type = "floppy"; + break; + case 6: /* Transparent SPC-2 devices */ + type = "scsi"; + break; + default: + break; + } + } + strscpy(to, len, type); + return type_num; +} + +static void set_scsi_type(char *to, const char *from, size_t len) { + int type_num; + char *eptr; + const char *type = "generic"; + + type_num = strtoul(from, &eptr, 0); + if (eptr != from) { + switch (type_num) { + case 0: + case 0xe: + type = "disk"; + break; + case 1: + type = "tape"; + break; + case 4: + case 7: + case 0xf: + type = "optical"; + break; + case 5: + type = "cd"; + break; + default: + break; + } + } + strscpy(to, len, type); +} + +#define USB_DT_DEVICE 0x01 +#define USB_DT_INTERFACE 0x04 + +static int dev_if_packed_info(sd_device *dev, char *ifs_str, size_t len) { + _cleanup_close_ int fd = -1; + ssize_t size; + unsigned char buf[18 + 65535]; + size_t pos = 0; + unsigned strpos = 0; + const char *filename, *syspath; + int r; + struct usb_interface_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t bInterfaceNumber; + uint8_t bAlternateSetting; + uint8_t bNumEndpoints; + uint8_t bInterfaceClass; + uint8_t bInterfaceSubClass; + uint8_t bInterfaceProtocol; + uint8_t iInterface; + } _packed_; + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + filename = strjoina(syspath, "/descriptors"); + fd = open(filename, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_device_debug_errno(dev, errno, "Failed to open \"%s\": %m", filename); + + size = read(fd, buf, sizeof(buf)); + if (size < 18) + return log_device_warning_errno(dev, SYNTHETIC_ERRNO(EIO), + "Short read from \"%s\"", filename); + assert((size_t) size <= sizeof buf); + + ifs_str[0] = '\0'; + while (pos + sizeof(struct usb_interface_descriptor) < (size_t) size && + strpos + 7 < len - 2) { + + struct usb_interface_descriptor *desc; + char if_str[8]; + + desc = (struct usb_interface_descriptor *) (buf + pos); + if (desc->bLength < 3) + break; + if (desc->bLength > size - sizeof(struct usb_interface_descriptor)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EIO), + "Corrupt data read from \"%s\"", filename); + pos += desc->bLength; + + if (desc->bDescriptorType != USB_DT_INTERFACE) + continue; + + if (snprintf(if_str, 8, ":%02x%02x%02x", + desc->bInterfaceClass, + desc->bInterfaceSubClass, + desc->bInterfaceProtocol) != 7) + continue; + + if (strstr(ifs_str, if_str)) + continue; + + memcpy(&ifs_str[strpos], if_str, 8), + strpos += 7; + } + + if (strpos > 0) { + ifs_str[strpos++] = ':'; + ifs_str[strpos++] = '\0'; + } + + return 0; +} + +/* + * A unique USB identification is generated like this: + * + * 1.) Get the USB device type from InterfaceClass and InterfaceSubClass + * 2.) If the device type is 'Mass-Storage/SPC-2' or 'Mass-Storage/RBC', + * use the SCSI vendor and model as USB-Vendor and USB-model. + * 3.) Otherwise, use the USB manufacturer and product as + * USB-Vendor and USB-model. Any non-printable characters + * in those strings will be skipped; a slash '/' will be converted + * into a full stop '.'. + * 4.) If that fails, too, we will use idVendor and idProduct + * as USB-Vendor and USB-model. + * 5.) The USB identification is the USB-vendor and USB-model + * string concatenated with an underscore '_'. + * 6.) If the device supplies a serial number, this number + * is concatenated with the identification with an underscore '_'. + */ +static int builtin_usb_id(sd_device *dev, int argc, char *argv[], bool test) { + char vendor_str[64] = ""; + char vendor_str_enc[256]; + const char *vendor_id; + char model_str[64] = ""; + char model_str_enc[256]; + const char *product_id; + char serial_str[UTIL_NAME_SIZE] = ""; + char packed_if_str[UTIL_NAME_SIZE] = ""; + char revision_str[64] = ""; + char type_str[64] = ""; + char instance_str[64] = ""; + const char *ifnum = NULL; + const char *driver = NULL; + char serial[256]; + + sd_device *dev_interface, *dev_usb; + const char *if_class, *if_subclass; + int if_class_num; + int protocol = 0; + size_t l; + char *s; + + const char *syspath, *sysname, *devtype, *interface_syspath; + int r; + + assert(dev); + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return r; + + /* shortcut, if we are called directly for a "usb_device" type */ + if (sd_device_get_devtype(dev, &devtype) >= 0 && streq(devtype, "usb_device")) { + dev_if_packed_info(dev, packed_if_str, sizeof(packed_if_str)); + dev_usb = dev; + goto fallback; + } + + /* usb interface directory */ + r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &dev_interface); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to access usb_interface: %m"); + + r = sd_device_get_syspath(dev_interface, &interface_syspath); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to get syspath: %m"); + (void) sd_device_get_sysattr_value(dev_interface, "bInterfaceNumber", &ifnum); + (void) sd_device_get_sysattr_value(dev_interface, "driver", &driver); + + r = sd_device_get_sysattr_value(dev_interface, "bInterfaceClass", &if_class); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to get bInterfaceClass attribute: %m"); + + if_class_num = strtoul(if_class, NULL, 16); + if (if_class_num == 8) { + /* mass storage */ + if (sd_device_get_sysattr_value(dev_interface, "bInterfaceSubClass", &if_subclass) >= 0) + protocol = set_usb_mass_storage_ifsubtype(type_str, if_subclass, sizeof(type_str)-1); + } else + set_usb_iftype(type_str, if_class_num, sizeof(type_str)-1); + + log_device_debug(dev_interface, "if_class:%d protocol:%d", if_class_num, protocol); + + /* usb device directory */ + r = sd_device_get_parent_with_subsystem_devtype(dev_interface, "usb", "usb_device", &dev_usb); + if (r < 0) + return log_device_debug_errno(dev_interface, r, "Failed to find parent 'usb' device"); + + /* all interfaces of the device in a single string */ + dev_if_packed_info(dev_usb, packed_if_str, sizeof(packed_if_str)); + + /* mass storage : SCSI or ATAPI */ + if (IN_SET(protocol, 6, 2)) { + sd_device *dev_scsi; + const char *scsi_sysname, *scsi_model, *scsi_vendor, *scsi_type, *scsi_rev; + int host, bus, target, lun; + + /* get scsi device */ + r = sd_device_get_parent_with_subsystem_devtype(dev, "scsi", "scsi_device", &dev_scsi); + if (r < 0) { + log_device_debug_errno(dev, r, "Unable to find parent SCSI device"); + goto fallback; + } + if (sd_device_get_sysname(dev_scsi, &scsi_sysname) < 0) + goto fallback; + if (sscanf(scsi_sysname, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4) { + log_device_debug(dev_scsi, "Invalid SCSI device"); + goto fallback; + } + + /* Generic SPC-2 device */ + r = sd_device_get_sysattr_value(dev_scsi, "vendor", &scsi_vendor); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI vendor attribute: %m"); + goto fallback; + } + udev_util_encode_string(scsi_vendor, vendor_str_enc, sizeof(vendor_str_enc)); + util_replace_whitespace(scsi_vendor, vendor_str, sizeof(vendor_str)-1); + util_replace_chars(vendor_str, NULL); + + r = sd_device_get_sysattr_value(dev_scsi, "model", &scsi_model); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI model attribute: %m"); + goto fallback; + } + udev_util_encode_string(scsi_model, model_str_enc, sizeof(model_str_enc)); + util_replace_whitespace(scsi_model, model_str, sizeof(model_str)-1); + util_replace_chars(model_str, NULL); + + r = sd_device_get_sysattr_value(dev_scsi, "type", &scsi_type); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI type attribute: %m"); + goto fallback; + } + set_scsi_type(type_str, scsi_type, sizeof(type_str)-1); + + r = sd_device_get_sysattr_value(dev_scsi, "rev", &scsi_rev); + if (r < 0) { + log_device_debug_errno(dev_scsi, r, "Failed to get SCSI revision attribute: %m"); + goto fallback; + } + util_replace_whitespace(scsi_rev, revision_str, sizeof(revision_str)-1); + util_replace_chars(revision_str, NULL); + + /* + * some broken devices have the same identifiers + * for all luns, export the target:lun number + */ + sprintf(instance_str, "%d:%d", target, lun); + } + +fallback: + r = sd_device_get_sysattr_value(dev_usb, "idVendor", &vendor_id); + if (r < 0) + return log_device_debug_errno(dev_usb, r, "Failed to get idVendor attribute: %m"); + + r = sd_device_get_sysattr_value(dev_usb, "idProduct", &product_id); + if (r < 0) + return log_device_debug_errno(dev_usb, r, "Failed to get idProduct attribute: %m"); + + /* fall back to USB vendor & device */ + if (vendor_str[0] == '\0') { + const char *usb_vendor; + + if (sd_device_get_sysattr_value(dev_usb, "manufacturer", &usb_vendor) < 0) + usb_vendor = vendor_id; + udev_util_encode_string(usb_vendor, vendor_str_enc, sizeof(vendor_str_enc)); + util_replace_whitespace(usb_vendor, vendor_str, sizeof(vendor_str)-1); + util_replace_chars(vendor_str, NULL); + } + + if (model_str[0] == '\0') { + const char *usb_model; + + if (sd_device_get_sysattr_value(dev_usb, "product", &usb_model) < 0) + usb_model = product_id; + udev_util_encode_string(usb_model, model_str_enc, sizeof(model_str_enc)); + util_replace_whitespace(usb_model, model_str, sizeof(model_str)-1); + util_replace_chars(model_str, NULL); + } + + if (revision_str[0] == '\0') { + const char *usb_rev; + + if (sd_device_get_sysattr_value(dev_usb, "bcdDevice", &usb_rev) >= 0) { + util_replace_whitespace(usb_rev, revision_str, sizeof(revision_str)-1); + util_replace_chars(revision_str, NULL); + } + } + + if (serial_str[0] == '\0') { + const char *usb_serial; + + if (sd_device_get_sysattr_value(dev_usb, "serial", &usb_serial) >= 0) { + const unsigned char *p; + + /* http://msdn.microsoft.com/en-us/library/windows/hardware/gg487321.aspx */ + for (p = (unsigned char *) usb_serial; *p != '\0'; p++) + if (*p < 0x20 || *p > 0x7f || *p == ',') { + usb_serial = NULL; + break; + } + + if (usb_serial) { + util_replace_whitespace(usb_serial, serial_str, sizeof(serial_str)-1); + util_replace_chars(serial_str, NULL); + } + } + } + + s = serial; + l = strpcpyl(&s, sizeof(serial), vendor_str, "_", model_str, NULL); + if (!isempty(serial_str)) + l = strpcpyl(&s, l, "_", serial_str, NULL); + + if (!isempty(instance_str)) + strpcpyl(&s, l, "-", instance_str, NULL); + + udev_builtin_add_property(dev, test, "ID_VENDOR", vendor_str); + udev_builtin_add_property(dev, test, "ID_VENDOR_ENC", vendor_str_enc); + udev_builtin_add_property(dev, test, "ID_VENDOR_ID", vendor_id); + udev_builtin_add_property(dev, test, "ID_MODEL", model_str); + udev_builtin_add_property(dev, test, "ID_MODEL_ENC", model_str_enc); + udev_builtin_add_property(dev, test, "ID_MODEL_ID", product_id); + udev_builtin_add_property(dev, test, "ID_REVISION", revision_str); + udev_builtin_add_property(dev, test, "ID_SERIAL", serial); + if (!isempty(serial_str)) + udev_builtin_add_property(dev, test, "ID_SERIAL_SHORT", serial_str); + if (!isempty(type_str)) + udev_builtin_add_property(dev, test, "ID_TYPE", type_str); + if (!isempty(instance_str)) + udev_builtin_add_property(dev, test, "ID_INSTANCE", instance_str); + udev_builtin_add_property(dev, test, "ID_BUS", "usb"); + if (!isempty(packed_if_str)) + udev_builtin_add_property(dev, test, "ID_USB_INTERFACES", packed_if_str); + if (ifnum) + udev_builtin_add_property(dev, test, "ID_USB_INTERFACE_NUM", ifnum); + if (driver) + udev_builtin_add_property(dev, test, "ID_USB_DRIVER", driver); + return 0; +} + +const UdevBuiltin udev_builtin_usb_id = { + .name = "usb_id", + .cmd = builtin_usb_id, + .help = "USB device properties", + .run_once = true, +}; diff --git a/src/udev/udev-builtin.c b/src/udev/udev-builtin.c new file mode 100644 index 0000000..80d1766 --- /dev/null +++ b/src/udev/udev-builtin.c @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <stdio.h> + +#include "device-private.h" +#include "device-util.h" +#include "string-util.h" +#include "strv.h" +#include "udev-builtin.h" + +static bool initialized; + +static const UdevBuiltin *const builtins[_UDEV_BUILTIN_MAX] = { +#if HAVE_BLKID + [UDEV_BUILTIN_BLKID] = &udev_builtin_blkid, +#endif + [UDEV_BUILTIN_BTRFS] = &udev_builtin_btrfs, + [UDEV_BUILTIN_HWDB] = &udev_builtin_hwdb, + [UDEV_BUILTIN_INPUT_ID] = &udev_builtin_input_id, + [UDEV_BUILTIN_KEYBOARD] = &udev_builtin_keyboard, +#if HAVE_KMOD + [UDEV_BUILTIN_KMOD] = &udev_builtin_kmod, +#endif + [UDEV_BUILTIN_NET_ID] = &udev_builtin_net_id, + [UDEV_BUILTIN_NET_LINK] = &udev_builtin_net_setup_link, + [UDEV_BUILTIN_PATH_ID] = &udev_builtin_path_id, + [UDEV_BUILTIN_USB_ID] = &udev_builtin_usb_id, +#if HAVE_ACL + [UDEV_BUILTIN_UACCESS] = &udev_builtin_uaccess, +#endif +}; + +void udev_builtin_init(void) { + unsigned i; + + if (initialized) + return; + + for (i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && builtins[i]->init) + builtins[i]->init(); + + initialized = true; +} + +void udev_builtin_exit(void) { + unsigned i; + + if (!initialized) + return; + + for (i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && builtins[i]->exit) + builtins[i]->exit(); + + initialized = false; +} + +bool udev_builtin_validate(void) { + unsigned i; + + for (i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && builtins[i]->validate && builtins[i]->validate()) + return true; + return false; +} + +void udev_builtin_list(void) { + unsigned i; + + for (i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i]) + fprintf(stderr, " %-14s %s\n", builtins[i]->name, builtins[i]->help); +} + +const char *udev_builtin_name(UdevBuiltinCommand cmd) { + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + + if (!builtins[cmd]) + return NULL; + + return builtins[cmd]->name; +} + +bool udev_builtin_run_once(UdevBuiltinCommand cmd) { + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + + if (!builtins[cmd]) + return false; + + return builtins[cmd]->run_once; +} + +UdevBuiltinCommand udev_builtin_lookup(const char *command) { + UdevBuiltinCommand i; + size_t n; + + assert(command); + + command += strspn(command, WHITESPACE); + n = strcspn(command, WHITESPACE); + for (i = 0; i < _UDEV_BUILTIN_MAX; i++) + if (builtins[i] && strneq(builtins[i]->name, command, n)) + return i; + + return _UDEV_BUILTIN_INVALID; +} + +int udev_builtin_run(sd_device *dev, UdevBuiltinCommand cmd, const char *command, bool test) { + _cleanup_strv_free_ char **argv = NULL; + int r; + + assert(dev); + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + assert(command); + + if (!builtins[cmd]) + return -EOPNOTSUPP; + + r = strv_split_full(&argv, command, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return r; + + /* we need '0' here to reset the internal state */ + optind = 0; + return builtins[cmd]->cmd(dev, strv_length(argv), argv, test); +} + +int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val) { + int r; + + assert(dev); + assert(key); + + r = device_add_property(dev, key, val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to add property '%s%s%s'", + key, val ? "=" : "", strempty(val)); + + if (test) + printf("%s=%s\n", key, strempty(val)); + + return 0; +} diff --git a/src/udev/udev-builtin.h b/src/udev/udev-builtin.h new file mode 100644 index 0000000..14d6406 --- /dev/null +++ b/src/udev/udev-builtin.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> + +#include "sd-device.h" + +typedef enum { +#if HAVE_BLKID + UDEV_BUILTIN_BLKID, +#endif + UDEV_BUILTIN_BTRFS, + UDEV_BUILTIN_HWDB, + UDEV_BUILTIN_INPUT_ID, + UDEV_BUILTIN_KEYBOARD, +#if HAVE_KMOD + UDEV_BUILTIN_KMOD, +#endif + UDEV_BUILTIN_NET_ID, + UDEV_BUILTIN_NET_LINK, + UDEV_BUILTIN_PATH_ID, + UDEV_BUILTIN_USB_ID, +#if HAVE_ACL + UDEV_BUILTIN_UACCESS, +#endif + _UDEV_BUILTIN_MAX, + _UDEV_BUILTIN_INVALID = -1, +} UdevBuiltinCommand; + +typedef struct UdevBuiltin { + const char *name; + int (*cmd)(sd_device *dev, int argc, char *argv[], bool test); + const char *help; + int (*init)(void); + void (*exit)(void); + bool (*validate)(void); + bool run_once; +} UdevBuiltin; + +#define PTR_TO_UDEV_BUILTIN_CMD(p) ((UdevBuiltinCommand) ((intptr_t) (p)-1)) +#define UDEV_BUILTIN_CMD_TO_PTR(u) ((void *) ((intptr_t) (u)+1)) + +#if HAVE_BLKID +extern const UdevBuiltin udev_builtin_blkid; +#endif +extern const UdevBuiltin udev_builtin_btrfs; +extern const UdevBuiltin udev_builtin_hwdb; +extern const UdevBuiltin udev_builtin_input_id; +extern const UdevBuiltin udev_builtin_keyboard; +#if HAVE_KMOD +extern const UdevBuiltin udev_builtin_kmod; +#endif +extern const UdevBuiltin udev_builtin_net_id; +extern const UdevBuiltin udev_builtin_net_setup_link; +extern const UdevBuiltin udev_builtin_path_id; +extern const UdevBuiltin udev_builtin_usb_id; +#if HAVE_ACL +extern const UdevBuiltin udev_builtin_uaccess; +#endif + +void udev_builtin_init(void); +void udev_builtin_exit(void); +UdevBuiltinCommand udev_builtin_lookup(const char *command); +const char *udev_builtin_name(UdevBuiltinCommand cmd); +bool udev_builtin_run_once(UdevBuiltinCommand cmd); +int udev_builtin_run(sd_device *dev, UdevBuiltinCommand cmd, const char *command, bool test); +void udev_builtin_list(void); +bool udev_builtin_validate(void); +int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val); +int udev_builtin_hwdb_lookup(sd_device *dev, const char *prefix, const char *modalias, + const char *filter, bool test); diff --git a/src/udev/udev-ctrl.c b/src/udev/udev-ctrl.c new file mode 100644 index 0000000..48355aa --- /dev/null +++ b/src/udev/udev-ctrl.c @@ -0,0 +1,393 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later + * + * libudev - interface to udev device information + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + */ + +#include <errno.h> +#include <poll.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <sys/un.h> +#include <unistd.h> + +#include "sd-event.h" + +#include "alloc-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "io-util.h" +#include "socket-util.h" +#include "strxcpyx.h" +#include "udev-ctrl.h" +#include "util.h" + +/* wire protocol magic must match */ +#define UDEV_CTRL_MAGIC 0xdead1dea + +struct udev_ctrl_msg_wire { + char version[16]; + unsigned magic; + enum udev_ctrl_msg_type type; + union udev_ctrl_msg_value value; +}; + +struct udev_ctrl { + unsigned n_ref; + int sock; + int sock_connect; + union sockaddr_union saddr; + socklen_t addrlen; + bool bound:1; + bool cleanup_socket:1; + bool connected:1; + bool maybe_disconnected:1; + sd_event *event; + sd_event_source *event_source; + sd_event_source *event_source_connect; + udev_ctrl_handler_t callback; + void *userdata; +}; + +int udev_ctrl_new_from_fd(struct udev_ctrl **ret, int fd) { + _cleanup_close_ int sock = -1; + struct udev_ctrl *uctrl; + + assert(ret); + + if (fd < 0) { + sock = socket(AF_LOCAL, SOCK_SEQPACKET|SOCK_NONBLOCK|SOCK_CLOEXEC, 0); + if (sock < 0) + return log_error_errno(errno, "Failed to create socket: %m"); + } + + uctrl = new(struct udev_ctrl, 1); + if (!uctrl) + return -ENOMEM; + + *uctrl = (struct udev_ctrl) { + .n_ref = 1, + .sock = fd >= 0 ? fd : TAKE_FD(sock), + .sock_connect = -1, + .bound = fd >= 0, + }; + + uctrl->saddr.un = (struct sockaddr_un) { + .sun_family = AF_UNIX, + .sun_path = "/run/udev/control", + }; + + uctrl->addrlen = SOCKADDR_UN_LEN(uctrl->saddr.un); + + *ret = TAKE_PTR(uctrl); + return 0; +} + +int udev_ctrl_enable_receiving(struct udev_ctrl *uctrl) { + int r; + + assert(uctrl); + + if (uctrl->bound) + return 0; + + r = bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen); + if (r < 0 && errno == EADDRINUSE) { + (void) sockaddr_un_unlink(&uctrl->saddr.un); + r = bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen); + } + + if (r < 0) + return log_error_errno(errno, "Failed to bind udev control socket: %m"); + + if (listen(uctrl->sock, 0) < 0) + return log_error_errno(errno, "Failed to listen udev control socket: %m"); + + uctrl->bound = true; + uctrl->cleanup_socket = true; + + return 0; +} + +static void udev_ctrl_disconnect(struct udev_ctrl *uctrl) { + if (!uctrl) + return; + + uctrl->event_source_connect = sd_event_source_unref(uctrl->event_source_connect); + uctrl->sock_connect = safe_close(uctrl->sock_connect); +} + +static struct udev_ctrl *udev_ctrl_free(struct udev_ctrl *uctrl) { + assert(uctrl); + + udev_ctrl_disconnect(uctrl); + + sd_event_source_unref(uctrl->event_source); + safe_close(uctrl->sock); + + sd_event_unref(uctrl->event); + return mfree(uctrl); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(struct udev_ctrl, udev_ctrl, udev_ctrl_free); + +int udev_ctrl_cleanup(struct udev_ctrl *uctrl) { + if (!uctrl) + return 0; + if (uctrl->cleanup_socket) + sockaddr_un_unlink(&uctrl->saddr.un); + return 0; +} + +int udev_ctrl_attach_event(struct udev_ctrl *uctrl, sd_event *event) { + int r; + + assert_return(uctrl, -EINVAL); + assert_return(!uctrl->event, -EBUSY); + + if (event) + uctrl->event = sd_event_ref(event); + else { + r = sd_event_default(&uctrl->event); + if (r < 0) + return r; + } + + return 0; +} + +sd_event_source *udev_ctrl_get_event_source(struct udev_ctrl *uctrl) { + assert(uctrl); + + return uctrl->event_source; +} + +static void udev_ctrl_disconnect_and_listen_again(struct udev_ctrl *uctrl) { + udev_ctrl_disconnect(uctrl); + udev_ctrl_unref(uctrl); + (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_ON); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl *, udev_ctrl_disconnect_and_listen_again); + +static int udev_ctrl_connection_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(udev_ctrl_disconnect_and_listen_againp) struct udev_ctrl *uctrl = NULL; + struct udev_ctrl_msg_wire msg_wire; + struct iovec iov = IOVEC_MAKE(&msg_wire, sizeof(struct udev_ctrl_msg_wire)); + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; + struct msghdr smsg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct cmsghdr *cmsg; + struct ucred *cred; + ssize_t size; + + assert(userdata); + + /* When UDEV_CTRL_EXIT is received, manager unref udev_ctrl object. + * To avoid the object freed, let's increment the refcount. */ + uctrl = udev_ctrl_ref(userdata); + + size = next_datagram_size_fd(fd); + if (size < 0) + return log_error_errno(size, "Failed to get size of message: %m"); + if (size == 0) + return 0; /* Client disconnects? */ + + size = recvmsg_safe(fd, &smsg, 0); + if (size == -EINTR) + return 0; + if (size < 0) + return log_error_errno(size, "Failed to receive ctrl message: %m"); + + cmsg_close_all(&smsg); + + cmsg = CMSG_FIRSTHDR(&smsg); + + if (!cmsg || cmsg->cmsg_type != SCM_CREDENTIALS) { + log_error("No sender credentials received, ignoring message"); + return 0; + } + + cred = (struct ucred *) CMSG_DATA(cmsg); + + if (cred->uid != 0) { + log_error("Invalid sender uid "UID_FMT", ignoring message", cred->uid); + return 0; + } + + if (msg_wire.magic != UDEV_CTRL_MAGIC) { + log_error("Message magic 0x%08x doesn't match, ignoring message", msg_wire.magic); + return 0; + } + + if (msg_wire.type == _UDEV_CTRL_END_MESSAGES) + return 0; + + if (uctrl->callback) + (void) uctrl->callback(uctrl, msg_wire.type, &msg_wire.value, uctrl->userdata); + + /* Do not disconnect and wait for next message. */ + uctrl = udev_ctrl_unref(uctrl); + return 0; +} + +static int udev_ctrl_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + struct udev_ctrl *uctrl = userdata; + _cleanup_close_ int sock = -1; + struct ucred ucred; + int r; + + assert(uctrl); + + sock = accept4(fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK); + if (sock < 0) { + if (ERRNO_IS_ACCEPT_AGAIN(errno)) + return 0; + + return log_error_errno(errno, "Failed to accept ctrl connection: %m"); + } + + /* check peer credential of connection */ + r = getpeercred(sock, &ucred); + if (r < 0) { + log_error_errno(r, "Failed to receive credentials of ctrl connection: %m"); + return 0; + } + + if (ucred.uid > 0) { + log_error("Invalid sender uid "UID_FMT", closing connection", ucred.uid); + return 0; + } + + /* enable receiving of the sender credentials in the messages */ + r = setsockopt_int(sock, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + log_warning_errno(r, "Failed to set SO_PASSCRED, ignoring: %m"); + + r = sd_event_add_io(uctrl->event, &uctrl->event_source_connect, sock, EPOLLIN, udev_ctrl_connection_event_handler, uctrl); + if (r < 0) { + log_error_errno(r, "Failed to create event source for udev control connection: %m"); + return 0; + } + + (void) sd_event_source_set_description(uctrl->event_source_connect, "udev-ctrl-connection"); + + /* Do not accept multiple connection. */ + (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_OFF); + + uctrl->sock_connect = TAKE_FD(sock); + return 0; +} + +int udev_ctrl_start(struct udev_ctrl *uctrl, udev_ctrl_handler_t callback, void *userdata) { + int r; + + assert(uctrl); + + if (!uctrl->event) { + r = udev_ctrl_attach_event(uctrl, NULL); + if (r < 0) + return r; + } + + r = udev_ctrl_enable_receiving(uctrl); + if (r < 0) + return r; + + uctrl->callback = callback; + uctrl->userdata = userdata; + + r = sd_event_add_io(uctrl->event, &uctrl->event_source, uctrl->sock, EPOLLIN, udev_ctrl_event_handler, uctrl); + if (r < 0) + return r; + + (void) sd_event_source_set_description(uctrl->event_source, "udev-ctrl"); + + return 0; +} + +int udev_ctrl_send(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, int intval, const char *buf) { + struct udev_ctrl_msg_wire ctrl_msg_wire = { + .version = "udev-" STRINGIFY(PROJECT_VERSION), + .magic = UDEV_CTRL_MAGIC, + .type = type, + }; + + if (uctrl->maybe_disconnected) + return -ENOANO; /* to distinguish this from other errors. */ + + if (buf) + strscpy(ctrl_msg_wire.value.buf, sizeof(ctrl_msg_wire.value.buf), buf); + else + ctrl_msg_wire.value.intval = intval; + + if (!uctrl->connected) { + if (connect(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen) < 0) + return -errno; + uctrl->connected = true; + } + + if (send(uctrl->sock, &ctrl_msg_wire, sizeof(ctrl_msg_wire), 0) < 0) + return -errno; + + if (type == UDEV_CTRL_EXIT) + uctrl->maybe_disconnected = true; + + return 0; +} + +int udev_ctrl_wait(struct udev_ctrl *uctrl, usec_t timeout) { + _cleanup_(sd_event_source_unrefp) sd_event_source *source_io = NULL, *source_timeout = NULL; + int r; + + assert(uctrl); + + if (uctrl->sock < 0) + return 0; + if (!uctrl->connected) + return 0; + + if (!uctrl->maybe_disconnected) { + r = udev_ctrl_send(uctrl, _UDEV_CTRL_END_MESSAGES, 0, NULL); + if (r < 0) + return r; + } + + if (timeout == 0) + return 0; + + if (!uctrl->event) { + r = udev_ctrl_attach_event(uctrl, NULL); + if (r < 0) + return r; + } + + r = sd_event_add_io(uctrl->event, &source_io, uctrl->sock, EPOLLIN, NULL, INT_TO_PTR(0)); + if (r < 0) + return r; + + (void) sd_event_source_set_description(source_io, "udev-ctrl-wait-io"); + + if (timeout != USEC_INFINITY) { + r = sd_event_add_time_relative( + uctrl->event, &source_timeout, clock_boottime_or_monotonic(), + timeout, + 0, NULL, INT_TO_PTR(-ETIMEDOUT)); + if (r < 0) + return r; + + (void) sd_event_source_set_description(source_timeout, "udev-ctrl-wait-timeout"); + } + + return sd_event_loop(uctrl->event); +} diff --git a/src/udev/udev-ctrl.h b/src/udev/udev-ctrl.h new file mode 100644 index 0000000..680fbf7 --- /dev/null +++ b/src/udev/udev-ctrl.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "sd-event.h" + +#include "macro.h" +#include "time-util.h" + +struct udev_ctrl; + +enum udev_ctrl_msg_type { + _UDEV_CTRL_END_MESSAGES, + UDEV_CTRL_SET_LOG_LEVEL, + UDEV_CTRL_STOP_EXEC_QUEUE, + UDEV_CTRL_START_EXEC_QUEUE, + UDEV_CTRL_RELOAD, + UDEV_CTRL_SET_ENV, + UDEV_CTRL_SET_CHILDREN_MAX, + UDEV_CTRL_PING, + UDEV_CTRL_EXIT, +}; + +union udev_ctrl_msg_value { + int intval; + char buf[256]; +}; + +typedef int (*udev_ctrl_handler_t)(struct udev_ctrl *udev_ctrl, enum udev_ctrl_msg_type type, + const union udev_ctrl_msg_value *value, void *userdata); + +int udev_ctrl_new_from_fd(struct udev_ctrl **ret, int fd); +static inline int udev_ctrl_new(struct udev_ctrl **ret) { + return udev_ctrl_new_from_fd(ret, -1); +} + +int udev_ctrl_enable_receiving(struct udev_ctrl *uctrl); +struct udev_ctrl *udev_ctrl_ref(struct udev_ctrl *uctrl); +struct udev_ctrl *udev_ctrl_unref(struct udev_ctrl *uctrl); +int udev_ctrl_cleanup(struct udev_ctrl *uctrl); +int udev_ctrl_attach_event(struct udev_ctrl *uctrl, sd_event *event); +int udev_ctrl_start(struct udev_ctrl *uctrl, udev_ctrl_handler_t callback, void *userdata); +sd_event_source *udev_ctrl_get_event_source(struct udev_ctrl *uctrl); + +int udev_ctrl_wait(struct udev_ctrl *uctrl, usec_t timeout); + +int udev_ctrl_send(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, int intval, const char *buf); +static inline int udev_ctrl_send_set_log_level(struct udev_ctrl *uctrl, int priority) { + return udev_ctrl_send(uctrl, UDEV_CTRL_SET_LOG_LEVEL, priority, NULL); +} + +static inline int udev_ctrl_send_stop_exec_queue(struct udev_ctrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_STOP_EXEC_QUEUE, 0, NULL); +} + +static inline int udev_ctrl_send_start_exec_queue(struct udev_ctrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_START_EXEC_QUEUE, 0, NULL); +} + +static inline int udev_ctrl_send_reload(struct udev_ctrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_RELOAD, 0, NULL); +} + +static inline int udev_ctrl_send_set_env(struct udev_ctrl *uctrl, const char *key) { + return udev_ctrl_send(uctrl, UDEV_CTRL_SET_ENV, 0, key); +} + +static inline int udev_ctrl_send_set_children_max(struct udev_ctrl *uctrl, int count) { + return udev_ctrl_send(uctrl, UDEV_CTRL_SET_CHILDREN_MAX, count, NULL); +} + +static inline int udev_ctrl_send_ping(struct udev_ctrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_PING, 0, NULL); +} + +static inline int udev_ctrl_send_exit(struct udev_ctrl *uctrl) { + return udev_ctrl_send(uctrl, UDEV_CTRL_EXIT, 0, NULL); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl*, udev_ctrl_unref); diff --git a/src/udev/udev-event.c b/src/udev/udev-event.c new file mode 100644 index 0000000..5159d19 --- /dev/null +++ b/src/udev/udev-event.c @@ -0,0 +1,1087 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <net/if.h> +#include <stddef.h> +#include <stdlib.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "sd-event.h" + +#include "alloc-util.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "format-util.h" +#include "libudev-util.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "rlimit-util.h" +#include "signal-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "udev-builtin.h" +#include "udev-event.h" +#include "udev-node.h" +#include "udev-util.h" +#include "udev-watch.h" +#include "user-util.h" + +typedef struct Spawn { + sd_device *device; + const char *cmd; + pid_t pid; + usec_t timeout_warn_usec; + usec_t timeout_usec; + int timeout_signal; + usec_t event_birth_usec; + bool accept_failure; + int fd_stdout; + int fd_stderr; + char *result; + size_t result_size; + size_t result_len; +} Spawn; + +UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl) { + UdevEvent *event; + + assert(dev); + + event = new(UdevEvent, 1); + if (!event) + return NULL; + + *event = (UdevEvent) { + .dev = sd_device_ref(dev), + .birth_usec = now(CLOCK_MONOTONIC), + .exec_delay_usec = exec_delay_usec, + .rtnl = sd_netlink_ref(rtnl), + .uid = UID_INVALID, + .gid = GID_INVALID, + .mode = MODE_INVALID, + }; + + return event; +} + +UdevEvent *udev_event_free(UdevEvent *event) { + if (!event) + return NULL; + + sd_device_unref(event->dev); + sd_device_unref(event->dev_db_clone); + sd_netlink_unref(event->rtnl); + ordered_hashmap_free_free_key(event->run_list); + ordered_hashmap_free_free_free(event->seclabel_list); + free(event->program_result); + free(event->name); + + return mfree(event); +} + +typedef enum { + FORMAT_SUBST_DEVNODE, + FORMAT_SUBST_ATTR, + FORMAT_SUBST_ENV, + FORMAT_SUBST_KERNEL, + FORMAT_SUBST_KERNEL_NUMBER, + FORMAT_SUBST_DRIVER, + FORMAT_SUBST_DEVPATH, + FORMAT_SUBST_ID, + FORMAT_SUBST_MAJOR, + FORMAT_SUBST_MINOR, + FORMAT_SUBST_RESULT, + FORMAT_SUBST_PARENT, + FORMAT_SUBST_NAME, + FORMAT_SUBST_LINKS, + FORMAT_SUBST_ROOT, + FORMAT_SUBST_SYS, + _FORMAT_SUBST_TYPE_MAX, + _FORMAT_SUBST_TYPE_INVALID = -1 +} FormatSubstitutionType; + +struct subst_map_entry { + const char *name; + const char fmt; + FormatSubstitutionType type; +}; + +static const struct subst_map_entry map[] = { + { .name = "devnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE }, + { .name = "tempnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE }, /* deprecated */ + { .name = "attr", .fmt = 's', .type = FORMAT_SUBST_ATTR }, + { .name = "sysfs", .fmt = 's', .type = FORMAT_SUBST_ATTR }, /* deprecated */ + { .name = "env", .fmt = 'E', .type = FORMAT_SUBST_ENV }, + { .name = "kernel", .fmt = 'k', .type = FORMAT_SUBST_KERNEL }, + { .name = "number", .fmt = 'n', .type = FORMAT_SUBST_KERNEL_NUMBER }, + { .name = "driver", .fmt = 'd', .type = FORMAT_SUBST_DRIVER }, + { .name = "devpath", .fmt = 'p', .type = FORMAT_SUBST_DEVPATH }, + { .name = "id", .fmt = 'b', .type = FORMAT_SUBST_ID }, + { .name = "major", .fmt = 'M', .type = FORMAT_SUBST_MAJOR }, + { .name = "minor", .fmt = 'm', .type = FORMAT_SUBST_MINOR }, + { .name = "result", .fmt = 'c', .type = FORMAT_SUBST_RESULT }, + { .name = "parent", .fmt = 'P', .type = FORMAT_SUBST_PARENT }, + { .name = "name", .fmt = 'D', .type = FORMAT_SUBST_NAME }, + { .name = "links", .fmt = 'L', .type = FORMAT_SUBST_LINKS }, + { .name = "root", .fmt = 'r', .type = FORMAT_SUBST_ROOT }, + { .name = "sys", .fmt = 'S', .type = FORMAT_SUBST_SYS }, +}; + +static const char *format_type_to_string(FormatSubstitutionType t) { + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (map[i].type == t) + return map[i].name; + return NULL; +} + +static char format_type_to_char(FormatSubstitutionType t) { + for (size_t i = 0; i < ELEMENTSOF(map); i++) + if (map[i].type == t) + return map[i].fmt; + return '\0'; +} + +static int get_subst_type(const char **str, bool strict, FormatSubstitutionType *ret_type, char ret_attr[static UTIL_PATH_SIZE]) { + const char *p = *str, *q = NULL; + size_t i; + + assert(str); + assert(*str); + assert(ret_type); + assert(ret_attr); + + if (*p == '$') { + p++; + if (*p == '$') { + *str = p; + return 0; + } + for (i = 0; i < ELEMENTSOF(map); i++) + if ((q = startswith(p, map[i].name))) + break; + } else if (*p == '%') { + p++; + if (*p == '%') { + *str = p; + return 0; + } + + for (i = 0; i < ELEMENTSOF(map); i++) + if (*p == map[i].fmt) { + q = p + 1; + break; + } + } else + return 0; + if (!q) + /* When 'strict' flag is set, then '$' and '%' must be escaped. */ + return strict ? -EINVAL : 0; + + if (*q == '{') { + const char *start, *end; + size_t len; + + start = q + 1; + end = strchr(start, '}'); + if (!end) + return -EINVAL; + + len = end - start; + if (len == 0 || len >= UTIL_PATH_SIZE) + return -EINVAL; + + strnscpy(ret_attr, UTIL_PATH_SIZE, start, len); + q = end + 1; + } else + *ret_attr = '\0'; + + *str = q; + *ret_type = map[i].type; + return 1; +} + +static int safe_atou_optional_plus(const char *s, unsigned *ret) { + const char *p; + int r; + + assert(s); + assert(ret); + + /* Returns 1 if plus, 0 if no plus, negative on error */ + + p = endswith(s, "+"); + if (p) + s = strndupa(s, p - s); + + r = safe_atou(s, ret); + if (r < 0) + return r; + + return !!p; +} + +static ssize_t udev_event_subst_format( + UdevEvent *event, + FormatSubstitutionType type, + const char *attr, + char *dest, + size_t l) { + sd_device *parent, *dev = event->dev; + const char *val = NULL; + char *s = dest; + int r; + + switch (type) { + case FORMAT_SUBST_DEVPATH: + r = sd_device_get_devpath(dev, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case FORMAT_SUBST_KERNEL: + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case FORMAT_SUBST_KERNEL_NUMBER: + r = sd_device_get_sysnum(dev, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case FORMAT_SUBST_ID: + if (!event->dev_parent) + goto null_terminate; + r = sd_device_get_sysname(event->dev_parent, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case FORMAT_SUBST_DRIVER: + if (!event->dev_parent) + goto null_terminate; + r = sd_device_get_driver(event->dev_parent, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case FORMAT_SUBST_MAJOR: + case FORMAT_SUBST_MINOR: { + dev_t devnum; + + r = sd_device_get_devnum(dev, &devnum); + if (r < 0 && r != -ENOENT) + return r; + l = strpcpyf(&s, l, "%u", r < 0 ? 0 : type == FORMAT_SUBST_MAJOR ? major(devnum) : minor(devnum)); + break; + } + case FORMAT_SUBST_RESULT: { + unsigned index = 0; /* 0 means whole string */ + bool has_plus; + + if (!event->program_result) + goto null_terminate; + + if (!isempty(attr)) { + r = safe_atou_optional_plus(attr, &index); + if (r < 0) + return r; + + has_plus = r; + } + + if (index == 0) + l = strpcpy(&s, l, event->program_result); + else { + const char *start, *p; + unsigned i; + + p = skip_leading_chars(event->program_result, NULL); + + for (i = 1; i < index; i++) { + while (*p && !strchr(WHITESPACE, *p)) + p++; + p = skip_leading_chars(p, NULL); + if (*p == '\0') + break; + } + if (i != index) { + log_device_debug(dev, "requested part of result string not found"); + goto null_terminate; + } + + start = p; + /* %c{2+} copies the whole string from the second part on */ + if (has_plus) + l = strpcpy(&s, l, start); + else { + while (*p && !strchr(WHITESPACE, *p)) + p++; + l = strnpcpy(&s, l, start, p - start); + } + } + break; + } + case FORMAT_SUBST_ATTR: { + char vbuf[UTIL_NAME_SIZE]; + int count; + + if (isempty(attr)) + return -EINVAL; + + /* try to read the value specified by "[dmi/id]product_name" */ + if (util_resolve_subsys_kernel(attr, vbuf, sizeof(vbuf), true) == 0) + val = vbuf; + + /* try to read the attribute the device */ + if (!val) + (void) sd_device_get_sysattr_value(dev, attr, &val); + + /* try to read the attribute of the parent device, other matches have selected */ + if (!val && event->dev_parent && event->dev_parent != dev) + (void) sd_device_get_sysattr_value(event->dev_parent, attr, &val); + + if (!val) + goto null_terminate; + + /* strip trailing whitespace, and replace unwanted characters */ + if (val != vbuf) + strscpy(vbuf, sizeof(vbuf), val); + delete_trailing_chars(vbuf, NULL); + count = util_replace_chars(vbuf, UDEV_ALLOWED_CHARS_INPUT); + if (count > 0) + log_device_debug(dev, "%i character(s) replaced", count); + l = strpcpy(&s, l, vbuf); + break; + } + case FORMAT_SUBST_PARENT: + r = sd_device_get_parent(dev, &parent); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + r = sd_device_get_devname(parent, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + l = strpcpy(&s, l, val + STRLEN("/dev/")); + break; + case FORMAT_SUBST_DEVNODE: + r = sd_device_get_devname(dev, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case FORMAT_SUBST_NAME: + if (event->name) + l = strpcpy(&s, l, event->name); + else if (sd_device_get_devname(dev, &val) >= 0) + l = strpcpy(&s, l, val + STRLEN("/dev/")); + else { + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + } + break; + case FORMAT_SUBST_LINKS: + FOREACH_DEVICE_DEVLINK(dev, val) + if (s == dest) + l = strpcpy(&s, l, val + STRLEN("/dev/")); + else + l = strpcpyl(&s, l, " ", val + STRLEN("/dev/"), NULL); + if (s == dest) + goto null_terminate; + break; + case FORMAT_SUBST_ROOT: + l = strpcpy(&s, l, "/dev"); + break; + case FORMAT_SUBST_SYS: + l = strpcpy(&s, l, "/sys"); + break; + case FORMAT_SUBST_ENV: + if (isempty(attr)) + return -EINVAL; + r = sd_device_get_property_value(dev, attr, &val); + if (r == -ENOENT) + goto null_terminate; + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + default: + assert_not_reached("Unknown format substitution type"); + } + + return s - dest; + +null_terminate: + *s = '\0'; + return 0; +} + +size_t udev_event_apply_format(UdevEvent *event, + const char *src, char *dest, size_t size, + bool replace_whitespace) { + const char *s = src; + int r; + + assert(event); + assert(event->dev); + assert(src); + assert(dest); + assert(size > 0); + + while (*s) { + FormatSubstitutionType type; + char attr[UTIL_PATH_SIZE]; + ssize_t subst_len; + + r = get_subst_type(&s, false, &type, attr); + if (r < 0) { + log_device_warning_errno(event->dev, r, "Invalid format string, ignoring: %s", src); + break; + } else if (r == 0) { + if (size < 2) /* need space for this char and the terminating NUL */ + break; + *dest++ = *s++; + size--; + continue; + } + + subst_len = udev_event_subst_format(event, type, attr, dest, size); + if (subst_len < 0) { + log_device_warning_errno(event->dev, subst_len, + "Failed to substitute variable '$%s' or apply format '%%%c', ignoring: %m", + format_type_to_string(type), format_type_to_char(type)); + break; + } + + /* FORMAT_SUBST_RESULT handles spaces itself */ + if (replace_whitespace && type != FORMAT_SUBST_RESULT) + /* util_replace_whitespace can replace in-place, + * and does nothing if subst_len == 0 */ + subst_len = util_replace_whitespace(dest, dest, subst_len); + + dest += subst_len; + size -= subst_len; + } + + assert(size >= 1); + *dest = '\0'; + return size; +} + +int udev_check_format(const char *value, size_t *offset, const char **hint) { + FormatSubstitutionType type; + const char *s = value; + char attr[UTIL_PATH_SIZE]; + int r; + + while (*s) { + r = get_subst_type(&s, true, &type, attr); + if (r < 0) { + if (offset) + *offset = s - value; + if (hint) + *hint = "invalid substitution type"; + return r; + } else if (r == 0) { + s++; + continue; + } + + if (IN_SET(type, FORMAT_SUBST_ATTR, FORMAT_SUBST_ENV) && isempty(attr)) { + if (offset) + *offset = s - value; + if (hint) + *hint = "attribute value missing"; + return -EINVAL; + } + + if (type == FORMAT_SUBST_RESULT && !isempty(attr)) { + unsigned i; + + r = safe_atou_optional_plus(attr, &i); + if (r < 0) { + if (offset) + *offset = s - value; + if (hint) + *hint = "attribute value not a valid number"; + return r; + } + } + } + + return 0; +} + +static int on_spawn_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Spawn *spawn = userdata; + char buf[4096], *p; + size_t size; + ssize_t l; + int r; + + assert(spawn); + assert(fd == spawn->fd_stdout || fd == spawn->fd_stderr); + assert(!spawn->result || spawn->result_len < spawn->result_size); + + if (fd == spawn->fd_stdout && spawn->result) { + p = spawn->result + spawn->result_len; + size = spawn->result_size - spawn->result_len; + } else { + p = buf; + size = sizeof(buf); + } + + l = read(fd, p, size - 1); + if (l < 0) { + if (errno == EAGAIN) + goto reenable; + + log_device_error_errno(spawn->device, errno, + "Failed to read stdout of '%s': %m", spawn->cmd); + + return 0; + } + + p[l] = '\0'; + if (fd == spawn->fd_stdout && spawn->result) + spawn->result_len += l; + + /* Log output only if we watch stderr. */ + if (l > 0 && spawn->fd_stderr >= 0) { + _cleanup_strv_free_ char **v = NULL; + char **q; + + v = strv_split_newlines(p); + if (!v) + return 0; + + STRV_FOREACH(q, v) + log_device_debug(spawn->device, "'%s'(%s) '%s'", spawn->cmd, + fd == spawn->fd_stdout ? "out" : "err", *q); + } + + + if (l == 0) + return 0; + + /* Re-enable the event source if we did not encounter EOF */ +reenable: + r = sd_event_source_set_enabled(s, SD_EVENT_ONESHOT); + if (r < 0) + log_device_error_errno(spawn->device, r, + "Failed to reactivate IO source of '%s'", spawn->cmd); + return 0; +} + +static int on_spawn_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + Spawn *spawn = userdata; + char timeout[FORMAT_TIMESPAN_MAX]; + + assert(spawn); + + kill_and_sigcont(spawn->pid, spawn->timeout_signal); + + log_device_error(spawn->device, "Spawned process '%s' ["PID_FMT"] timed out after %s, killing", + spawn->cmd, spawn->pid, + format_timespan(timeout, sizeof(timeout), spawn->timeout_usec, USEC_PER_SEC)); + + return 1; +} + +static int on_spawn_timeout_warning(sd_event_source *s, uint64_t usec, void *userdata) { + Spawn *spawn = userdata; + char timeout[FORMAT_TIMESPAN_MAX]; + + assert(spawn); + + log_device_warning(spawn->device, "Spawned process '%s' ["PID_FMT"] is taking longer than %s to complete", + spawn->cmd, spawn->pid, + format_timespan(timeout, sizeof(timeout), spawn->timeout_warn_usec, USEC_PER_SEC)); + + return 1; +} + +static int on_spawn_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata) { + Spawn *spawn = userdata; + int ret = -EIO; + + assert(spawn); + + switch (si->si_code) { + case CLD_EXITED: + if (si->si_status == 0) + log_device_debug(spawn->device, "Process '%s' succeeded.", spawn->cmd); + else + log_device_full(spawn->device, spawn->accept_failure ? LOG_DEBUG : LOG_WARNING, + "Process '%s' failed with exit code %i.", spawn->cmd, si->si_status); + ret = si->si_status; + break; + case CLD_KILLED: + case CLD_DUMPED: + log_device_error(spawn->device, "Process '%s' terminated by signal %s.", spawn->cmd, signal_to_string(si->si_status)); + break; + default: + log_device_error(spawn->device, "Process '%s' failed due to unknown reason.", spawn->cmd); + } + + sd_event_exit(sd_event_source_get_event(s), ret); + return 1; +} + +static int spawn_wait(Spawn *spawn) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *sigchld_source = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *stdout_source = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *stderr_source = NULL; + int r; + + assert(spawn); + + r = sd_event_new(&e); + if (r < 0) + return r; + + if (spawn->timeout_usec > 0) { + usec_t usec, age_usec; + + usec = now(CLOCK_MONOTONIC); + age_usec = usec - spawn->event_birth_usec; + if (age_usec < spawn->timeout_usec) { + if (spawn->timeout_warn_usec > 0 && + spawn->timeout_warn_usec < spawn->timeout_usec && + spawn->timeout_warn_usec > age_usec) { + spawn->timeout_warn_usec -= age_usec; + + r = sd_event_add_time(e, NULL, CLOCK_MONOTONIC, + usec + spawn->timeout_warn_usec, USEC_PER_SEC, + on_spawn_timeout_warning, spawn); + if (r < 0) + return r; + } + + spawn->timeout_usec -= age_usec; + + r = sd_event_add_time(e, NULL, CLOCK_MONOTONIC, + usec + spawn->timeout_usec, USEC_PER_SEC, on_spawn_timeout, spawn); + if (r < 0) + return r; + } + } + + if (spawn->fd_stdout >= 0) { + r = sd_event_add_io(e, &stdout_source, spawn->fd_stdout, EPOLLIN, on_spawn_io, spawn); + if (r < 0) + return r; + r = sd_event_source_set_enabled(stdout_source, SD_EVENT_ONESHOT); + if (r < 0) + return r; + } + + if (spawn->fd_stderr >= 0) { + r = sd_event_add_io(e, &stderr_source, spawn->fd_stderr, EPOLLIN, on_spawn_io, spawn); + if (r < 0) + return r; + r = sd_event_source_set_enabled(stderr_source, SD_EVENT_ONESHOT); + if (r < 0) + return r; + } + + r = sd_event_add_child(e, &sigchld_source, spawn->pid, WEXITED, on_spawn_sigchld, spawn); + if (r < 0) + return r; + /* SIGCHLD should be processed after IO is complete */ + r = sd_event_source_set_priority(sigchld_source, SD_EVENT_PRIORITY_NORMAL + 1); + if (r < 0) + return r; + + + return sd_event_loop(e); +} + +int udev_event_spawn(UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + bool accept_failure, + const char *cmd, + char *result, size_t ressize) { + _cleanup_close_pair_ int outpipe[2] = {-1, -1}, errpipe[2] = {-1, -1}; + _cleanup_strv_free_ char **argv = NULL; + char **envp = NULL; + Spawn spawn; + pid_t pid; + int r; + + assert(event); + assert(event->dev); + assert(result || ressize == 0); + + /* pipes from child to parent */ + if (result || log_get_max_level() >= LOG_INFO) + if (pipe2(outpipe, O_NONBLOCK|O_CLOEXEC) != 0) + return log_device_error_errno(event->dev, errno, + "Failed to create pipe for command '%s': %m", cmd); + + if (log_get_max_level() >= LOG_INFO) + if (pipe2(errpipe, O_NONBLOCK|O_CLOEXEC) != 0) + return log_device_error_errno(event->dev, errno, + "Failed to create pipe for command '%s': %m", cmd); + + r = strv_split_full(&argv, cmd, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return log_device_error_errno(event->dev, r, "Failed to split command: %m"); + + if (isempty(argv[0])) + return log_device_error_errno(event->dev, SYNTHETIC_ERRNO(EINVAL), + "Invalid command '%s'", cmd); + + /* allow programs in /usr/lib/udev/ to be called without the path */ + if (!path_is_absolute(argv[0])) { + char *program; + + program = path_join(UDEVLIBEXECDIR, argv[0]); + if (!program) + return log_oom(); + + free_and_replace(argv[0], program); + } + + r = device_get_properties_strv(event->dev, &envp); + if (r < 0) + return log_device_error_errno(event->dev, r, "Failed to get device properties"); + + log_device_debug(event->dev, "Starting '%s'", cmd); + + r = safe_fork("(spawn)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return log_device_error_errno(event->dev, r, + "Failed to fork() to execute command '%s': %m", cmd); + if (r == 0) { + if (rearrange_stdio(-1, outpipe[WRITE_END], errpipe[WRITE_END]) < 0) + _exit(EXIT_FAILURE); + + (void) close_all_fds(NULL, 0); + (void) rlimit_nofile_safe(); + + execve(argv[0], argv, envp); + _exit(EXIT_FAILURE); + } + + /* parent closed child's ends of pipes */ + outpipe[WRITE_END] = safe_close(outpipe[WRITE_END]); + errpipe[WRITE_END] = safe_close(errpipe[WRITE_END]); + + spawn = (Spawn) { + .device = event->dev, + .cmd = cmd, + .pid = pid, + .accept_failure = accept_failure, + .timeout_warn_usec = udev_warn_timeout(timeout_usec), + .timeout_usec = timeout_usec, + .timeout_signal = timeout_signal, + .event_birth_usec = event->birth_usec, + .fd_stdout = outpipe[READ_END], + .fd_stderr = errpipe[READ_END], + .result = result, + .result_size = ressize, + }; + r = spawn_wait(&spawn); + if (r < 0) + return log_device_error_errno(event->dev, r, + "Failed to wait for spawned command '%s': %m", cmd); + + if (result) + result[spawn.result_len] = '\0'; + + return r; /* 0 for success, and positive if the program failed */ +} + +static int rename_netif(UdevEvent *event) { + sd_device *dev = event->dev; + const char *oldname; + int ifindex, r; + + if (!event->name) + return 0; /* No new name is requested. */ + + r = sd_device_get_sysname(dev, &oldname); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get sysname: %m"); + + if (streq(event->name, oldname)) + return 0; /* The interface name is already requested name. */ + + if (!device_for_action(dev, DEVICE_ACTION_ADD)) + return 0; /* Rename the interface only when it is added. */ + + r = sd_device_get_ifindex(dev, &ifindex); + if (r == -ENOENT) + return 0; /* Device is not a network interface. */ + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get ifindex: %m"); + + /* Set ID_RENAMING boolean property here, and drop it in the corresponding move uevent later. */ + r = device_add_property(dev, "ID_RENAMING", "1"); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to add 'ID_RENAMING' property: %m"); + + r = device_rename(dev, event->name); + if (r < 0) + return log_device_warning_errno(dev, r, "Failed to update properties with new name '%s': %m", event->name); + + /* Also set ID_RENAMING boolean property to cloned sd_device object and save it to database + * before calling rtnl_set_link_name(). Otherwise, clients (e.g., systemd-networkd) may receive + * RTM_NEWLINK netlink message before the database is updated. */ + r = device_add_property(event->dev_db_clone, "ID_RENAMING", "1"); + if (r < 0) + return log_device_warning_errno(event->dev_db_clone, r, "Failed to add 'ID_RENAMING' property: %m"); + + r = device_update_db(event->dev_db_clone); + if (r < 0) + return log_device_debug_errno(event->dev_db_clone, r, "Failed to update database under /run/udev/data/: %m"); + + r = rtnl_set_link_name(&event->rtnl, ifindex, event->name); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to rename network interface %i from '%s' to '%s': %m", + ifindex, oldname, event->name); + + log_device_debug(dev, "Network interface %i is renamed from '%s' to '%s'", ifindex, oldname, event->name); + + return 1; +} + +static int update_devnode(UdevEvent *event) { + sd_device *dev = event->dev; + int r; + + r = sd_device_get_devnum(dev, NULL); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get devnum: %m"); + + /* remove/update possible left-over symlinks from old database entry */ + (void) udev_node_update_old_links(dev, event->dev_db_clone); + + if (!uid_is_valid(event->uid)) { + r = device_get_devnode_uid(dev, &event->uid); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(dev, r, "Failed to get devnode UID: %m"); + } + + if (!gid_is_valid(event->gid)) { + r = device_get_devnode_gid(dev, &event->gid); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(dev, r, "Failed to get devnode GID: %m"); + } + + if (event->mode == MODE_INVALID) { + r = device_get_devnode_mode(dev, &event->mode); + if (r < 0 && r != -ENOENT) + return log_device_error_errno(dev, r, "Failed to get devnode mode: %m"); + } + if (event->mode == MODE_INVALID && gid_is_valid(event->gid) && event->gid > 0) + /* If group is set, but mode is not set, "upgrade" mode for the group. */ + event->mode = 0660; + + bool apply_mac = device_for_action(dev, DEVICE_ACTION_ADD); + + return udev_node_add(dev, apply_mac, event->mode, event->uid, event->gid, event->seclabel_list); +} + +static void event_execute_rules_on_remove( + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRules *rules) { + + sd_device *dev = event->dev; + int r; + + r = device_read_db_internal(dev, true); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to read database under /run/udev/data/: %m"); + + r = device_tag_index(dev, NULL, false); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to remove corresponding tag files under /run/udev/tag/, ignoring: %m"); + + r = device_delete_db(dev); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to delete database under /run/udev/data/, ignoring: %m"); + + if (sd_device_get_devnum(dev, NULL) >= 0) + (void) udev_watch_end(dev); + + (void) udev_rules_apply_to_event(rules, event, timeout_usec, timeout_signal, properties_list); + + if (sd_device_get_devnum(dev, NULL) >= 0) + (void) udev_node_remove(dev); +} + +static int udev_event_on_move(sd_device *dev) { + int r; + + /* Drop previously added property */ + r = device_add_property(dev, "ID_RENAMING", NULL); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to remove 'ID_RENAMING' property: %m"); + + return 0; +} + +static int copy_all_tags(sd_device *d, sd_device *s) { + const char *tag; + int r; + + assert(d); + + if (!s) + return 0; + + FOREACH_DEVICE_TAG(s, tag) { + r = device_add_tag(d, tag, false); + if (r < 0) + return r; + } + + return 0; +} + +int udev_event_execute_rules(UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRules *rules) { + const char *subsystem; + DeviceAction action; + sd_device *dev; + int r; + + assert(event); + assert(rules); + + dev = event->dev; + + r = sd_device_get_subsystem(dev, &subsystem); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get subsystem: %m"); + + r = device_get_action(dev, &action); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get ACTION: %m"); + + if (action == DEVICE_ACTION_REMOVE) { + event_execute_rules_on_remove(event, timeout_usec, timeout_signal, properties_list, rules); + return 0; + } + + r = device_clone_with_db(dev, &event->dev_db_clone); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to clone sd_device object: %m"); + + r = copy_all_tags(dev, event->dev_db_clone); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to copy all tags from old database entry, ignoring: %m"); + + if (sd_device_get_devnum(dev, NULL) >= 0) + /* Disable watch during event processing. */ + (void) udev_watch_end(event->dev_db_clone); + + if (action == DEVICE_ACTION_MOVE) { + r = udev_event_on_move(event->dev); + if (r < 0) + return r; + } + + r = udev_rules_apply_to_event(rules, event, timeout_usec, timeout_signal, properties_list); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to apply udev rules: %m"); + + r = rename_netif(event); + if (r < 0) + return r; + + r = update_devnode(event); + if (r < 0) + return r; + + /* preserve old, or get new initialization timestamp */ + r = device_ensure_usec_initialized(dev, event->dev_db_clone); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to set initialization timestamp: %m"); + + /* (re)write database file */ + r = device_tag_index(dev, event->dev_db_clone, true); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to update tags under /run/udev/tag/: %m"); + + r = device_update_db(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m"); + + /* Yes, we run update_devnode() twice, because in the first invocation, that is before update of udev database, + * it could happen that two contenders are replacing each other's symlink. Hence we run it again to make sure + * symlinks point to devices that claim them with the highest priority. */ + r = update_devnode(event); + if (r < 0) + return r; + + device_set_is_initialized(dev); + + return 0; +} + +void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec, int timeout_signal) { + const char *command; + void *val; + int r; + + ORDERED_HASHMAP_FOREACH_KEY(val, command, event->run_list) { + UdevBuiltinCommand builtin_cmd = PTR_TO_UDEV_BUILTIN_CMD(val); + + if (builtin_cmd != _UDEV_BUILTIN_INVALID) { + log_device_debug(event->dev, "Running built-in command \"%s\"", command); + r = udev_builtin_run(event->dev, builtin_cmd, command, false); + if (r < 0) + log_device_debug_errno(event->dev, r, "Failed to run built-in command \"%s\", ignoring: %m", command); + } else { + if (event->exec_delay_usec > 0) { + char buf[FORMAT_TIMESPAN_MAX]; + + log_device_debug(event->dev, "Delaying execution of \"%s\" for %s.", + command, format_timespan(buf, sizeof(buf), event->exec_delay_usec, USEC_PER_SEC)); + (void) usleep(event->exec_delay_usec); + } + + log_device_debug(event->dev, "Running command \"%s\"", command); + + r = udev_event_spawn(event, timeout_usec, timeout_signal, false, command, NULL, 0); + if (r < 0) + log_device_warning_errno(event->dev, r, "Failed to execute '%s', ignoring: %m", command); + else if (r > 0) /* returned value is positive when program fails */ + log_device_debug(event->dev, "Command \"%s\" returned %d (error), ignoring.", command, r); + } + } +} diff --git a/src/udev/udev-event.h b/src/udev/udev-event.h new file mode 100644 index 0000000..8647828 --- /dev/null +++ b/src/udev/udev-event.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +/* + * Copyright © 2003 Greg Kroah-Hartman <greg@kroah.com> + */ + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "hashmap.h" +#include "macro.h" +#include "udev-rules.h" +#include "udev-util.h" +#include "util.h" + +#define READ_END 0 +#define WRITE_END 1 + +typedef struct UdevEvent { + sd_device *dev; + sd_device *dev_parent; + sd_device *dev_db_clone; + char *name; + char *program_result; + mode_t mode; + uid_t uid; + gid_t gid; + OrderedHashmap *seclabel_list; + OrderedHashmap *run_list; + usec_t exec_delay_usec; + usec_t birth_usec; + sd_netlink *rtnl; + unsigned builtin_run; + unsigned builtin_ret; + UdevRuleEscapeType esc:8; + bool inotify_watch:1; + bool inotify_watch_final:1; + bool group_final:1; + bool owner_final:1; + bool mode_final:1; + bool name_final:1; + bool devlink_final:1; + bool run_final:1; +} UdevEvent; + +UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl); +UdevEvent *udev_event_free(UdevEvent *event); +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevEvent*, udev_event_free); + +size_t udev_event_apply_format(UdevEvent *event, + const char *src, char *dest, size_t size, + bool replace_whitespace); +int udev_check_format(const char *value, size_t *offset, const char **hint); +int udev_event_spawn(UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + bool accept_failure, + const char *cmd, char *result, size_t ressize); +int udev_event_execute_rules(UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRules *rules); +void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec, int timeout_signal); + +static inline usec_t udev_warn_timeout(usec_t timeout_usec) { + return DIV_ROUND_UP(timeout_usec, 3); +} diff --git a/src/udev/udev-node.c b/src/udev/udev-node.c new file mode 100644 index 0000000..b8b93ee --- /dev/null +++ b/src/udev/udev-node.c @@ -0,0 +1,528 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stddef.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "device-nodes.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "fs-util.h" +#include "libudev-util.h" +#include "mkdir.h" +#include "path-util.h" +#include "selinux-util.h" +#include "smack-util.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-node.h" +#include "user-util.h" + +#define LINK_UPDATE_MAX_RETRIES 128 + +static int node_symlink(sd_device *dev, const char *node, const char *slink) { + _cleanup_free_ char *slink_dirname = NULL, *target = NULL; + const char *id_filename, *slink_tmp; + struct stat stats; + int r; + + assert(dev); + assert(node); + assert(slink); + + slink_dirname = dirname_malloc(slink); + if (!slink_dirname) + return log_oom(); + + /* use relative link */ + r = path_make_relative(slink_dirname, node, &target); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get relative path from '%s' to '%s': %m", slink, node); + + /* preserve link with correct target, do not replace node of other device */ + if (lstat(slink, &stats) == 0) { + if (S_ISBLK(stats.st_mode) || S_ISCHR(stats.st_mode)) + return log_device_error_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP), + "Conflicting device node '%s' found, link to '%s' will not be created.", slink, node); + else if (S_ISLNK(stats.st_mode)) { + _cleanup_free_ char *buf = NULL; + + if (readlink_malloc(slink, &buf) >= 0 && + streq(target, buf)) { + log_device_debug(dev, "Preserve already existing symlink '%s' to '%s'", slink, target); + (void) label_fix(slink, LABEL_IGNORE_ENOENT); + (void) utimensat(AT_FDCWD, slink, NULL, AT_SYMLINK_NOFOLLOW); + return 0; + } + } + } else { + log_device_debug(dev, "Creating symlink '%s' to '%s'", slink, target); + do { + r = mkdir_parents_label(slink, 0755); + if (!IN_SET(r, 0, -ENOENT)) + break; + mac_selinux_create_file_prepare(slink, S_IFLNK); + if (symlink(target, slink) < 0) + r = -errno; + mac_selinux_create_file_clear(); + } while (r == -ENOENT); + if (r == 0) + return 0; + if (r < 0) + log_device_debug_errno(dev, r, "Failed to create symlink '%s' to '%s', trying to replace '%s': %m", slink, target, slink); + } + + log_device_debug(dev, "Atomically replace '%s'", slink); + r = device_get_id_filename(dev, &id_filename); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get id_filename: %m"); + slink_tmp = strjoina(slink, ".tmp-", id_filename); + (void) unlink(slink_tmp); + do { + r = mkdir_parents_label(slink_tmp, 0755); + if (!IN_SET(r, 0, -ENOENT)) + break; + mac_selinux_create_file_prepare(slink_tmp, S_IFLNK); + if (symlink(target, slink_tmp) < 0) + r = -errno; + mac_selinux_create_file_clear(); + } while (r == -ENOENT); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to create symlink '%s' to '%s': %m", slink_tmp, target); + + if (rename(slink_tmp, slink) < 0) { + r = log_device_error_errno(dev, errno, "Failed to rename '%s' to '%s': %m", slink_tmp, slink); + (void) unlink(slink_tmp); + } else + /* Tell caller that we replaced already existing symlink. */ + r = 1; + + return r; +} + +/* find device node of device with highest priority */ +static int link_find_prioritized(sd_device *dev, bool add, const char *stackdir, char **ret) { + _cleanup_closedir_ DIR *dir = NULL; + _cleanup_free_ char *target = NULL; + struct dirent *dent; + int r, priority = 0; + + assert(!add || dev); + assert(stackdir); + assert(ret); + + if (add) { + const char *devnode; + + r = device_get_devlink_priority(dev, &priority); + if (r < 0) + return r; + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return r; + + target = strdup(devnode); + if (!target) + return -ENOMEM; + } + + dir = opendir(stackdir); + if (!dir) { + if (target) { + *ret = TAKE_PTR(target); + return 0; + } + + return -errno; + } + + FOREACH_DIRENT_ALL(dent, dir, break) { + _cleanup_(sd_device_unrefp) sd_device *dev_db = NULL; + const char *devnode, *id_filename; + int db_prio = 0; + + if (dent->d_name[0] == '\0') + break; + if (dent->d_name[0] == '.') + continue; + + log_device_debug(dev, "Found '%s' claiming '%s'", dent->d_name, stackdir); + + if (device_get_id_filename(dev, &id_filename) < 0) + continue; + + /* did we find ourself? */ + if (streq(dent->d_name, id_filename)) + continue; + + if (sd_device_new_from_device_id(&dev_db, dent->d_name) < 0) + continue; + + if (sd_device_get_devname(dev_db, &devnode) < 0) + continue; + + if (device_get_devlink_priority(dev_db, &db_prio) < 0) + continue; + + if (target && db_prio <= priority) + continue; + + log_device_debug(dev_db, "Device claims priority %i for '%s'", db_prio, stackdir); + + r = free_and_strdup(&target, devnode); + if (r < 0) + return r; + priority = db_prio; + } + + if (!target) + return -ENOENT; + + *ret = TAKE_PTR(target); + return 0; +} + +/* manage "stack of names" with possibly specified device priorities */ +static int link_update(sd_device *dev, const char *slink, bool add) { + _cleanup_free_ char *filename = NULL, *dirname = NULL; + char name_enc[PATH_MAX]; + const char *id_filename; + int i, r, retries; + + assert(dev); + assert(slink); + + r = device_get_id_filename(dev, &id_filename); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get id_filename: %m"); + + util_path_encode(slink + STRLEN("/dev"), name_enc, sizeof(name_enc)); + dirname = path_join("/run/udev/links/", name_enc); + if (!dirname) + return log_oom(); + filename = path_join(dirname, id_filename); + if (!filename) + return log_oom(); + + if (!add) { + if (unlink(filename) == 0) + (void) rmdir(dirname); + } else + for (;;) { + _cleanup_close_ int fd = -1; + + r = mkdir_parents(filename, 0755); + if (!IN_SET(r, 0, -ENOENT)) + return r; + + fd = open(filename, O_WRONLY|O_CREAT|O_CLOEXEC|O_TRUNC|O_NOFOLLOW, 0444); + if (fd >= 0) + break; + if (errno != ENOENT) + return -errno; + } + + /* If the database entry is not written yet we will just do one iteration and possibly wrong symlink + * will be fixed in the second invocation. */ + retries = sd_device_get_is_initialized(dev) > 0 ? LINK_UPDATE_MAX_RETRIES : 1; + + for (i = 0; i < retries; i++) { + _cleanup_free_ char *target = NULL; + struct stat st1 = {}, st2 = {}; + + r = stat(dirname, &st1); + if (r < 0 && errno != ENOENT) + return -errno; + + r = link_find_prioritized(dev, add, dirname, &target); + if (r == -ENOENT) { + log_device_debug(dev, "No reference left, removing '%s'", slink); + if (unlink(slink) == 0) + (void) rmdir_parents(slink, "/"); + + break; + } else if (r < 0) + return log_device_error_errno(dev, r, "Failed to determine highest priority symlink: %m"); + + r = node_symlink(dev, target, slink); + if (r < 0) { + (void) unlink(filename); + break; + } else if (r == 1) + /* We have replaced already existing symlink, possibly there is some other device trying + * to claim the same symlink. Let's do one more iteration to give us a chance to fix + * the error if other device actually claims the symlink with higher priority. */ + continue; + + /* Skip the second stat() if the first failed, stat_inode_unmodified() would return false regardless. */ + if ((st1.st_mode & S_IFMT) != 0) { + r = stat(dirname, &st2); + if (r < 0 && errno != ENOENT) + return -errno; + + if (stat_inode_unmodified(&st1, &st2)) + break; + } + } + + return i < LINK_UPDATE_MAX_RETRIES ? 0 : -ELOOP; +} + +int udev_node_update_old_links(sd_device *dev, sd_device *dev_old) { + const char *name, *devpath; + int r; + + assert(dev); + assert(dev_old); + + r = sd_device_get_devpath(dev, &devpath); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devpath: %m"); + + /* update possible left-over symlinks */ + FOREACH_DEVICE_DEVLINK(dev_old, name) { + const char *name_current; + bool found = false; + + /* check if old link name still belongs to this device */ + FOREACH_DEVICE_DEVLINK(dev, name_current) + if (streq(name, name_current)) { + found = true; + break; + } + + if (found) + continue; + + log_device_debug(dev, "Updating old name, '%s' no longer belonging to '%s'", + name, devpath); + link_update(dev, name, false); + } + + return 0; +} + +static int node_permissions_apply(sd_device *dev, bool apply_mac, + mode_t mode, uid_t uid, gid_t gid, + OrderedHashmap *seclabel_list) { + const char *devnode, *subsystem, *id_filename = NULL; + bool apply_mode, apply_uid, apply_gid; + _cleanup_close_ int node_fd = -1; + struct stat stats; + dev_t devnum; + int r; + + assert(dev); + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devname: %m"); + r = sd_device_get_subsystem(dev, &subsystem); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get subsystem: %m"); + r = sd_device_get_devnum(dev, &devnum); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devnum: %m"); + (void) device_get_id_filename(dev, &id_filename); + + if (streq(subsystem, "block")) + mode |= S_IFBLK; + else + mode |= S_IFCHR; + + node_fd = open(devnode, O_PATH|O_NOFOLLOW|O_CLOEXEC); + if (node_fd < 0) { + if (errno == ENOENT) { + log_device_debug_errno(dev, errno, "Device node %s is missing, skipping handling.", devnode); + return 0; /* This is necessarily racey, so ignore missing the device */ + } + + return log_device_debug_errno(dev, errno, "Cannot open node %s: %m", devnode); + } + + if (fstat(node_fd, &stats) < 0) + return log_device_debug_errno(dev, errno, "cannot stat() node %s: %m", devnode); + + if ((mode != MODE_INVALID && (stats.st_mode & S_IFMT) != (mode & S_IFMT)) || stats.st_rdev != devnum) { + log_device_debug(dev, "Found node '%s' with non-matching devnum %s, skipping handling.", + devnode, id_filename); + return 0; /* We might process a device that already got replaced by the time we have a look + * at it, handle this gracefully and step away. */ + } + + apply_mode = mode != MODE_INVALID && (stats.st_mode & 0777) != (mode & 0777); + apply_uid = uid_is_valid(uid) && stats.st_uid != uid; + apply_gid = gid_is_valid(gid) && stats.st_gid != gid; + + if (apply_mode || apply_uid || apply_gid || apply_mac) { + bool selinux = false, smack = false; + const char *name, *label; + + if (apply_mode || apply_uid || apply_gid) { + log_device_debug(dev, "Setting permissions %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o", + devnode, + uid_is_valid(uid) ? uid : stats.st_uid, + gid_is_valid(gid) ? gid : stats.st_gid, + mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); + + r = fchmod_and_chown(node_fd, mode, uid, gid); + if (r < 0) + log_device_full_errno(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, + "Failed to set owner/mode of %s to uid=" UID_FMT + ", gid=" GID_FMT ", mode=%#o: %m", + devnode, + uid_is_valid(uid) ? uid : stats.st_uid, + gid_is_valid(gid) ? gid : stats.st_gid, + mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); + } else + log_device_debug(dev, "Preserve permissions of %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o", + devnode, + uid_is_valid(uid) ? uid : stats.st_uid, + gid_is_valid(gid) ? gid : stats.st_gid, + mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); + + /* apply SECLABEL{$module}=$label */ + ORDERED_HASHMAP_FOREACH_KEY(label, name, seclabel_list) { + int q; + + if (streq(name, "selinux")) { + selinux = true; + + q = mac_selinux_apply_fd(node_fd, devnode, label); + if (q < 0) + log_device_full_errno(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q, + "SECLABEL: failed to set SELinux label '%s': %m", label); + else + log_device_debug(dev, "SECLABEL: set SELinux label '%s'", label); + + } else if (streq(name, "smack")) { + smack = true; + + q = mac_smack_apply_fd(node_fd, SMACK_ATTR_ACCESS, label); + if (q < 0) + log_device_full_errno(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q, + "SECLABEL: failed to set SMACK label '%s': %m", label); + else + log_device_debug(dev, "SECLABEL: set SMACK label '%s'", label); + + } else + log_device_error(dev, "SECLABEL: unknown subsystem, ignoring '%s'='%s'", name, label); + } + + /* set the defaults */ + if (!selinux) + (void) mac_selinux_fix_fd(node_fd, devnode, LABEL_IGNORE_ENOENT); + if (!smack) + (void) mac_smack_apply_fd(node_fd, SMACK_ATTR_ACCESS, NULL); + } + + /* always update timestamp when we re-use the node, like on media change events */ + r = futimens_opath(node_fd, NULL); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to adjust timestamp of node %s: %m", devnode); + + return r; +} + +static int xsprintf_dev_num_path_from_sd_device(sd_device *dev, char **ret) { + char filename[DEV_NUM_PATH_MAX], *s; + const char *subsystem; + dev_t devnum; + int r; + + assert(ret); + + r = sd_device_get_subsystem(dev, &subsystem); + if (r < 0) + return r; + + r = sd_device_get_devnum(dev, &devnum); + if (r < 0) + return r; + + xsprintf_dev_num_path(filename, + streq(subsystem, "block") ? "block" : "char", + devnum); + + s = strdup(filename); + if (!s) + return -ENOMEM; + + *ret = s; + return 0; +} + +int udev_node_add(sd_device *dev, bool apply, + mode_t mode, uid_t uid, gid_t gid, + OrderedHashmap *seclabel_list) { + const char *devnode, *devlink; + _cleanup_free_ char *filename = NULL; + int r; + + assert(dev); + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devnode: %m"); + + if (DEBUG_LOGGING) { + const char *id_filename = NULL; + + (void) device_get_id_filename(dev, &id_filename); + log_device_debug(dev, "Handling device node '%s', devnum=%s", devnode, strnull(id_filename)); + } + + r = node_permissions_apply(dev, apply, mode, uid, gid, seclabel_list); + if (r < 0) + return r; + + r = xsprintf_dev_num_path_from_sd_device(dev, &filename); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device path: %m"); + + /* always add /dev/{block,char}/$major:$minor */ + (void) node_symlink(dev, devnode, filename); + + /* create/update symlinks, add symlinks to name index */ + FOREACH_DEVICE_DEVLINK(dev, devlink) { + r = link_update(dev, devlink, true); + if (r < 0) + log_device_info_errno(dev, r, "Failed to update device symlinks: %m"); + } + + return 0; +} + +int udev_node_remove(sd_device *dev) { + _cleanup_free_ char *filename = NULL; + const char *devlink; + int r; + + assert(dev); + + /* remove/update symlinks, remove symlinks from name index */ + FOREACH_DEVICE_DEVLINK(dev, devlink) { + r = link_update(dev, devlink, false); + if (r < 0) + log_device_info_errno(dev, r, "Failed to update device symlinks: %m"); + } + + r = xsprintf_dev_num_path_from_sd_device(dev, &filename); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device path: %m"); + + /* remove /dev/{block,char}/$major:$minor */ + (void) unlink(filename); + + return 0; +} diff --git a/src/udev/udev-node.h b/src/udev/udev-node.h new file mode 100644 index 0000000..84c7e45 --- /dev/null +++ b/src/udev/udev-node.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdbool.h> +#include <sys/types.h> + +#include "sd-device.h" + +#include "hashmap.h" + +int udev_node_add(sd_device *dev, bool apply, + mode_t mode, uid_t uid, gid_t gid, + OrderedHashmap *seclabel_list); +int udev_node_remove(sd_device *dev); +int udev_node_update_old_links(sd_device *dev, sd_device *dev_old); diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c new file mode 100644 index 0000000..ef6a0c1 --- /dev/null +++ b/src/udev/udev-rules.c @@ -0,0 +1,2393 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <ctype.h> + +#include "alloc-util.h" +#include "architecture.h" +#include "conf-files.h" +#include "def.h" +#include "device-util.h" +#include "dirent-util.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "glob-util.h" +#include "libudev-util.h" +#include "list.h" +#include "mkdir.h" +#include "nulstr-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "stat-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "sysctl-util.h" +#include "udev-builtin.h" +#include "udev-event.h" +#include "udev-rules.h" +#include "user-util.h" +#include "virt.h" + +#define RULES_DIRS (const char* const*) CONF_PATHS_STRV("udev/rules.d") + +typedef enum { + OP_MATCH, /* == */ + OP_NOMATCH, /* != */ + OP_ADD, /* += */ + OP_REMOVE, /* -= */ + OP_ASSIGN, /* = */ + OP_ASSIGN_FINAL, /* := */ + _OP_TYPE_MAX, + _OP_TYPE_INVALID = -1 +} UdevRuleOperatorType; + +typedef enum { + MATCH_TYPE_EMPTY, /* empty string */ + MATCH_TYPE_PLAIN, /* no special characters */ + MATCH_TYPE_PLAIN_WITH_EMPTY, /* no special characters with empty string, e.g., "|foo" */ + MATCH_TYPE_GLOB, /* shell globs ?,*,[] */ + MATCH_TYPE_GLOB_WITH_EMPTY, /* shell globs ?,*,[] with empty string, e.g., "|foo*" */ + MATCH_TYPE_SUBSYSTEM, /* "subsystem", "bus", or "class" */ + _MATCH_TYPE_MAX, + _MATCH_TYPE_INVALID = -1 +} UdevRuleMatchType; + +typedef enum { + SUBST_TYPE_PLAIN, /* no substitution */ + SUBST_TYPE_FORMAT, /* % or $ */ + SUBST_TYPE_SUBSYS, /* "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */ + _SUBST_TYPE_MAX, + _SUBST_TYPE_INVALID = -1 +} UdevRuleSubstituteType; + +typedef enum { + /* lvalues which take match or nomatch operator */ + TK_M_ACTION, /* string, device_get_action() */ + TK_M_DEVPATH, /* path, sd_device_get_devpath() */ + TK_M_KERNEL, /* string, sd_device_get_sysname() */ + TK_M_DEVLINK, /* strv, sd_device_get_devlink_first(), sd_device_get_devlink_next() */ + TK_M_NAME, /* string, name of network interface */ + TK_M_ENV, /* string, device property, takes key through attribute */ + TK_M_CONST, /* string, system-specific hard-coded constant */ + TK_M_TAG, /* strv, sd_device_get_tag_first(), sd_device_get_tag_next() */ + TK_M_SUBSYSTEM, /* string, sd_device_get_subsystem() */ + TK_M_DRIVER, /* string, sd_device_get_driver() */ + TK_M_ATTR, /* string, takes filename through attribute, sd_device_get_sysattr_value(), util_resolve_subsys_kernel(), etc. */ + TK_M_SYSCTL, /* string, takes kernel parameter through attribute */ + + /* matches parent parameters */ + TK_M_PARENTS_KERNEL, /* string */ + TK_M_PARENTS_SUBSYSTEM, /* string */ + TK_M_PARENTS_DRIVER, /* string */ + TK_M_PARENTS_ATTR, /* string */ + TK_M_PARENTS_TAG, /* strv */ + + TK_M_TEST, /* path, optionally mode_t can be specified by attribute, test the existence of a file */ + TK_M_PROGRAM, /* string, execute a program */ + TK_M_IMPORT_FILE, /* path */ + TK_M_IMPORT_PROGRAM, /* string, import properties from the result of program */ + TK_M_IMPORT_BUILTIN, /* string, import properties from the result of built-in command */ + TK_M_IMPORT_DB, /* string, import properties from database */ + TK_M_IMPORT_CMDLINE, /* string, kernel command line */ + TK_M_IMPORT_PARENT, /* string, parent property */ + TK_M_RESULT, /* string, result of TK_M_PROGRAM */ + +#define _TK_M_MAX (TK_M_RESULT + 1) +#define _TK_A_MIN _TK_M_MAX + + /* lvalues which take one of assign operators */ + TK_A_OPTIONS_STRING_ESCAPE_NONE, /* no argument */ + TK_A_OPTIONS_STRING_ESCAPE_REPLACE, /* no argument */ + TK_A_OPTIONS_DB_PERSIST, /* no argument */ + TK_A_OPTIONS_INOTIFY_WATCH, /* boolean */ + TK_A_OPTIONS_DEVLINK_PRIORITY, /* int */ + TK_A_OWNER, /* user name */ + TK_A_GROUP, /* group name */ + TK_A_MODE, /* mode string */ + TK_A_OWNER_ID, /* uid_t */ + TK_A_GROUP_ID, /* gid_t */ + TK_A_MODE_ID, /* mode_t */ + TK_A_TAG, /* string */ + TK_A_OPTIONS_STATIC_NODE, /* device path, /dev/... */ + TK_A_SECLABEL, /* string with attribute */ + TK_A_ENV, /* string with attribute */ + TK_A_NAME, /* ifname */ + TK_A_DEVLINK, /* string */ + TK_A_ATTR, /* string with attribute */ + TK_A_SYSCTL, /* string with attribute */ + TK_A_RUN_BUILTIN, /* string */ + TK_A_RUN_PROGRAM, /* string */ + + _TK_TYPE_MAX, + _TK_TYPE_INVALID = -1, +} UdevRuleTokenType; + +typedef enum { + LINE_HAS_NAME = 1 << 0, /* has NAME= */ + LINE_HAS_DEVLINK = 1 << 1, /* has SYMLINK=, OWNER=, GROUP= or MODE= */ + LINE_HAS_STATIC_NODE = 1 << 2, /* has OPTIONS=static_node */ + LINE_HAS_GOTO = 1 << 3, /* has GOTO= */ + LINE_HAS_LABEL = 1 << 4, /* has LABEL= */ + LINE_UPDATE_SOMETHING = 1 << 5, /* has other TK_A_* or TK_M_IMPORT tokens */ +} UdevRuleLineType; + +typedef struct UdevRuleFile UdevRuleFile; +typedef struct UdevRuleLine UdevRuleLine; +typedef struct UdevRuleToken UdevRuleToken; + +struct UdevRuleToken { + UdevRuleTokenType type:8; + UdevRuleOperatorType op:8; + UdevRuleMatchType match_type:8; + UdevRuleSubstituteType attr_subst_type:7; + bool attr_match_remove_trailing_whitespace:1; + const char *value; + void *data; + LIST_FIELDS(UdevRuleToken, tokens); +}; + +struct UdevRuleLine { + char *line; + unsigned line_number; + UdevRuleLineType type; + + const char *label; + const char *goto_label; + UdevRuleLine *goto_line; + + UdevRuleFile *rule_file; + UdevRuleToken *current_token; + LIST_HEAD(UdevRuleToken, tokens); + LIST_FIELDS(UdevRuleLine, rule_lines); +}; + +struct UdevRuleFile { + char *filename; + UdevRuleLine *current_line; + LIST_HEAD(UdevRuleLine, rule_lines); + LIST_FIELDS(UdevRuleFile, rule_files); +}; + +struct UdevRules { + usec_t dirs_ts_usec; + ResolveNameTiming resolve_name_timing; + Hashmap *known_users; + Hashmap *known_groups; + UdevRuleFile *current_file; + LIST_HEAD(UdevRuleFile, rule_files); +}; + +/*** Logging helpers ***/ + +#define log_rule_full_errno(device, rules, level, error, fmt, ...) \ + ({ \ + UdevRules *_r = (rules); \ + UdevRuleFile *_f = _r ? _r->current_file : NULL; \ + UdevRuleLine *_l = _f ? _f->current_line : NULL; \ + const char *_n = _f ? _f->filename : NULL; \ + \ + log_device_full_errno(device, level, error, "%s:%u " fmt, \ + strna(_n), _l ? _l->line_number : 0, \ + ##__VA_ARGS__); \ + }) + +#define log_rule_full(device, rules, level, ...) (void) log_rule_full_errno(device, rules, level, 0, __VA_ARGS__) + +#define log_rule_debug(device, rules, ...) log_rule_full_errno(device, rules, LOG_DEBUG, 0, __VA_ARGS__) +#define log_rule_info(device, rules, ...) log_rule_full(device, rules, LOG_INFO, __VA_ARGS__) +#define log_rule_notice(device, rules, ...) log_rule_full(device, rules, LOG_NOTICE, __VA_ARGS__) +#define log_rule_warning(device, rules, ...) log_rule_full(device, rules, LOG_WARNING, __VA_ARGS__) +#define log_rule_error(device, rules, ...) log_rule_full(device, rules, LOG_ERR, __VA_ARGS__) + +#define log_rule_debug_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_DEBUG, error, __VA_ARGS__) +#define log_rule_info_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_INFO, error, __VA_ARGS__) +#define log_rule_notice_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_NOTICE, error, __VA_ARGS__) +#define log_rule_warning_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_WARNING, error, __VA_ARGS__) +#define log_rule_error_errno(device, rules, error, ...) log_rule_full_errno(device, rules, LOG_ERR, error, __VA_ARGS__) + +#define log_token_full_errno(rules, level, error, ...) log_rule_full_errno(NULL, rules, level, error, __VA_ARGS__) +#define log_token_full(rules, level, ...) (void) log_token_full_errno(rules, level, 0, __VA_ARGS__) + +#define log_token_debug(rules, ...) log_token_full_errno(rules, LOG_DEBUG, 0, __VA_ARGS__) +#define log_token_info(rules, ...) log_token_full(rules, LOG_INFO, __VA_ARGS__) +#define log_token_notice(rules, ...) log_token_full(rules, LOG_NOTICE, __VA_ARGS__) +#define log_token_warning(rules, ...) log_token_full(rules, LOG_WARNING, __VA_ARGS__) +#define log_token_error(rules, ...) log_token_full(rules, LOG_ERR, __VA_ARGS__) + +#define log_token_debug_errno(rules, error, ...) log_token_full_errno(rules, LOG_DEBUG, error, __VA_ARGS__) +#define log_token_info_errno(rules, error, ...) log_token_full_errno(rules, LOG_INFO, error, __VA_ARGS__) +#define log_token_notice_errno(rules, error, ...) log_token_full_errno(rules, LOG_NOTICE, error, __VA_ARGS__) +#define log_token_warning_errno(rules, error, ...) log_token_full_errno(rules, LOG_WARNING, error, __VA_ARGS__) +#define log_token_error_errno(rules, error, ...) log_token_full_errno(rules, LOG_ERR, error, __VA_ARGS__) + +#define _log_token_invalid(rules, key, type) \ + log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), \ + "Invalid %s for %s.", type, key) + +#define log_token_invalid_op(rules, key) _log_token_invalid(rules, key, "operator") +#define log_token_invalid_attr(rules, key) _log_token_invalid(rules, key, "attribute") + +#define log_token_invalid_attr_format(rules, key, attr, offset, hint) \ + log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), \ + "Invalid attribute \"%s\" for %s (char %zu: %s), ignoring, but please fix it.", \ + attr, key, offset, hint) +#define log_token_invalid_value(rules, key, value, offset, hint) \ + log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), \ + "Invalid value \"%s\" for %s (char %zu: %s), ignoring, but please fix it.", \ + value, key, offset, hint) + +static void log_unknown_owner(sd_device *dev, UdevRules *rules, int error, const char *entity, const char *name) { + if (IN_SET(abs(error), ENOENT, ESRCH)) + log_rule_error(dev, rules, "Unknown %s '%s', ignoring", entity, name); + else + log_rule_error_errno(dev, rules, error, "Failed to resolve %s '%s', ignoring: %m", entity, name); +} + +/*** Other functions ***/ + +static void udev_rule_token_free(UdevRuleToken *token) { + free(token); +} + +static void udev_rule_line_clear_tokens(UdevRuleLine *rule_line) { + UdevRuleToken *i, *next; + + assert(rule_line); + + LIST_FOREACH_SAFE(tokens, i, next, rule_line->tokens) + udev_rule_token_free(i); + + rule_line->tokens = NULL; +} + +static void udev_rule_line_free(UdevRuleLine *rule_line) { + if (!rule_line) + return; + + udev_rule_line_clear_tokens(rule_line); + + if (rule_line->rule_file) { + if (rule_line->rule_file->current_line == rule_line) + rule_line->rule_file->current_line = rule_line->rule_lines_prev; + + LIST_REMOVE(rule_lines, rule_line->rule_file->rule_lines, rule_line); + } + + free(rule_line->line); + free(rule_line); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleLine*, udev_rule_line_free); + +static void udev_rule_file_free(UdevRuleFile *rule_file) { + UdevRuleLine *i, *next; + + if (!rule_file) + return; + + LIST_FOREACH_SAFE(rule_lines, i, next, rule_file->rule_lines) + udev_rule_line_free(i); + + free(rule_file->filename); + free(rule_file); +} + +UdevRules *udev_rules_free(UdevRules *rules) { + UdevRuleFile *i, *next; + + if (!rules) + return NULL; + + LIST_FOREACH_SAFE(rule_files, i, next, rules->rule_files) + udev_rule_file_free(i); + + hashmap_free_free_key(rules->known_users); + hashmap_free_free_key(rules->known_groups); + return mfree(rules); +} + +static int rule_resolve_user(UdevRules *rules, const char *name, uid_t *ret) { + _cleanup_free_ char *n = NULL; + uid_t uid; + void *val; + int r; + + assert(rules); + assert(name); + + val = hashmap_get(rules->known_users, name); + if (val) { + *ret = PTR_TO_UID(val); + return 0; + } + + r = get_user_creds(&name, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) { + log_unknown_owner(NULL, rules, r, "user", name); + *ret = UID_INVALID; + return 0; + } + + n = strdup(name); + if (!n) + return -ENOMEM; + + r = hashmap_ensure_allocated(&rules->known_users, &string_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(rules->known_users, n, UID_TO_PTR(uid)); + if (r < 0) + return r; + + TAKE_PTR(n); + *ret = uid; + return 0; +} + +static int rule_resolve_group(UdevRules *rules, const char *name, gid_t *ret) { + _cleanup_free_ char *n = NULL; + gid_t gid; + void *val; + int r; + + assert(rules); + assert(name); + + val = hashmap_get(rules->known_groups, name); + if (val) { + *ret = PTR_TO_GID(val); + return 0; + } + + r = get_group_creds(&name, &gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) { + log_unknown_owner(NULL, rules, r, "group", name); + *ret = GID_INVALID; + return 0; + } + + n = strdup(name); + if (!n) + return -ENOMEM; + + r = hashmap_ensure_allocated(&rules->known_groups, &string_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(rules->known_groups, n, GID_TO_PTR(gid)); + if (r < 0) + return r; + + TAKE_PTR(n); + *ret = gid; + return 0; +} + +static UdevRuleSubstituteType rule_get_substitution_type(const char *str) { + assert(str); + + if (str[0] == '[') + return SUBST_TYPE_SUBSYS; + if (strchr(str, '%') || strchr(str, '$')) + return SUBST_TYPE_FORMAT; + return SUBST_TYPE_PLAIN; +} + +static void rule_line_append_token(UdevRuleLine *rule_line, UdevRuleToken *token) { + assert(rule_line); + assert(token); + + if (rule_line->current_token) + LIST_APPEND(tokens, rule_line->current_token, token); + else + LIST_APPEND(tokens, rule_line->tokens, token); + + rule_line->current_token = token; +} + +static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, UdevRuleOperatorType op, char *value, void *data) { + UdevRuleToken *token; + UdevRuleMatchType match_type = _MATCH_TYPE_INVALID; + UdevRuleSubstituteType subst_type = _SUBST_TYPE_INVALID; + bool remove_trailing_whitespace = false; + size_t len; + + assert(rule_line); + assert(type >= 0 && type < _TK_TYPE_MAX); + assert(op >= 0 && op < _OP_TYPE_MAX); + + if (type < _TK_M_MAX) { + assert(value); + assert(IN_SET(op, OP_MATCH, OP_NOMATCH)); + + if (type == TK_M_SUBSYSTEM && STR_IN_SET(value, "subsystem", "bus", "class")) + match_type = MATCH_TYPE_SUBSYSTEM; + else if (isempty(value)) + match_type = MATCH_TYPE_EMPTY; + else if (streq(value, "?*")) { + /* Convert KEY=="?*" -> KEY!="" */ + match_type = MATCH_TYPE_EMPTY; + op = op == OP_MATCH ? OP_NOMATCH : OP_MATCH; + } else if (string_is_glob(value)) + match_type = MATCH_TYPE_GLOB; + else + match_type = MATCH_TYPE_PLAIN; + + if (type < TK_M_TEST || type == TK_M_RESULT) { + /* Convert value string to nulstr. */ + bool bar = true, empty = false; + char *a, *b; + + for (a = b = value; *a != '\0'; a++) { + if (*a != '|') { + *b++ = *a; + bar = false; + } else { + if (bar) + empty = true; + else + *b++ = '\0'; + bar = true; + } + } + *b = '\0'; + + /* Make sure the value is end, so NULSTR_FOREACH can read correct match */ + if (b < a) + b[1] = '\0'; + + if (bar) + empty = true; + + if (empty) { + if (match_type == MATCH_TYPE_GLOB) + match_type = MATCH_TYPE_GLOB_WITH_EMPTY; + if (match_type == MATCH_TYPE_PLAIN) + match_type = MATCH_TYPE_PLAIN_WITH_EMPTY; + } + } + } + + if (IN_SET(type, TK_M_ATTR, TK_M_PARENTS_ATTR)) { + assert(value); + assert(data); + + len = strlen(value); + if (len > 0 && !isspace(value[len - 1])) + remove_trailing_whitespace = true; + + subst_type = rule_get_substitution_type(data); + } + + token = new(UdevRuleToken, 1); + if (!token) + return -ENOMEM; + + *token = (UdevRuleToken) { + .type = type, + .op = op, + .value = value, + .data = data, + .match_type = match_type, + .attr_subst_type = subst_type, + .attr_match_remove_trailing_whitespace = remove_trailing_whitespace, + }; + + rule_line_append_token(rule_line, token); + + if (token->type == TK_A_NAME) + SET_FLAG(rule_line->type, LINE_HAS_NAME, true); + + else if (IN_SET(token->type, TK_A_DEVLINK, + TK_A_OWNER, TK_A_GROUP, TK_A_MODE, + TK_A_OWNER_ID, TK_A_GROUP_ID, TK_A_MODE_ID)) + SET_FLAG(rule_line->type, LINE_HAS_DEVLINK, true); + + else if (token->type == TK_A_OPTIONS_STATIC_NODE) + SET_FLAG(rule_line->type, LINE_HAS_STATIC_NODE, true); + + else if (token->type >= _TK_A_MIN || + IN_SET(token->type, TK_M_PROGRAM, + TK_M_IMPORT_FILE, TK_M_IMPORT_PROGRAM, TK_M_IMPORT_BUILTIN, + TK_M_IMPORT_DB, TK_M_IMPORT_CMDLINE, TK_M_IMPORT_PARENT)) + SET_FLAG(rule_line->type, LINE_UPDATE_SOMETHING, true); + + return 0; +} + +static void check_value_format_and_warn(UdevRules *rules, const char *key, const char *value, bool nonempty) { + size_t offset; + const char *hint; + + if (nonempty && isempty(value)) + log_token_invalid_value(rules, key, value, (size_t) 0, "empty value"); + else if (udev_check_format(value, &offset, &hint) < 0) + log_token_invalid_value(rules, key, value, offset + 1, hint); +} + +static int check_attr_format_and_warn(UdevRules *rules, const char *key, const char *value) { + size_t offset; + const char *hint; + + if (isempty(value)) + return log_token_invalid_attr(rules, key); + if (udev_check_format(value, &offset, &hint) < 0) + log_token_invalid_attr_format(rules, key, value, offset + 1, hint); + return 0; +} + +static int parse_token(UdevRules *rules, const char *key, char *attr, UdevRuleOperatorType op, char *value) { + bool is_match = IN_SET(op, OP_MATCH, OP_NOMATCH); + UdevRuleLine *rule_line; + int r; + + assert(rules); + assert(rules->current_file); + assert(rules->current_file->current_line); + assert(key); + assert(value); + + rule_line = rules->current_file->current_line; + + if (streq(key, "ACTION")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_ACTION, op, value, NULL); + } else if (streq(key, "DEVPATH")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_DEVPATH, op, value, NULL); + } else if (streq(key, "KERNEL")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_KERNEL, op, value, NULL); + } else if (streq(key, "SYMLINK")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + + if (!is_match) { + check_value_format_and_warn(rules, key, value, false); + r = rule_line_add_token(rule_line, TK_A_DEVLINK, op, value, NULL); + } else + r = rule_line_add_token(rule_line, TK_M_DEVLINK, op, value, NULL); + } else if (streq(key, "NAME")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ADD) { + log_token_warning(rules, "%s key takes '==', '!=', '=', or ':=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + if (streq(value, "%k")) + return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), + "NAME=\"%%k\" is ignored, as it breaks kernel supplied names."); + if (isempty(value)) + return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), + "Ignoring NAME=\"\", as udev will not delete any device nodes."); + check_value_format_and_warn(rules, key, value, false); + + r = rule_line_add_token(rule_line, TK_A_NAME, op, value, NULL); + } else + r = rule_line_add_token(rule_line, TK_M_NAME, op, value, NULL); + } else if (streq(key, "ENV")) { + if (isempty(attr)) + return log_token_invalid_attr(rules, key); + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ASSIGN_FINAL) { + log_token_warning(rules, "%s key takes '==', '!=', '=', or '+=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + if (STR_IN_SET(attr, + "ACTION", "DEVLINKS", "DEVNAME", "DEVPATH", "DEVTYPE", "DRIVER", + "IFINDEX", "MAJOR", "MINOR", "SEQNUM", "SUBSYSTEM", "TAGS")) + return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), + "Invalid ENV attribute. '%s' cannot be set.", attr); + + check_value_format_and_warn(rules, key, value, false); + + r = rule_line_add_token(rule_line, TK_A_ENV, op, value, attr); + } else + r = rule_line_add_token(rule_line, TK_M_ENV, op, value, attr); + } else if (streq(key, "CONST")) { + if (isempty(attr) || !STR_IN_SET(attr, "arch", "virt")) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + r = rule_line_add_token(rule_line, TK_M_CONST, op, value, attr); + } else if (streq(key, "TAG")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (op == OP_ASSIGN_FINAL) { + log_token_warning(rules, "%s key takes '==', '!=', '=', or '+=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + check_value_format_and_warn(rules, key, value, true); + + r = rule_line_add_token(rule_line, TK_A_TAG, op, value, NULL); + } else + r = rule_line_add_token(rule_line, TK_M_TAG, op, value, NULL); + } else if (streq(key, "SUBSYSTEM")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + if (STR_IN_SET(value, "bus", "class")) + log_token_warning(rules, "'%s' must be specified as 'subsystem'; please fix it", value); + + r = rule_line_add_token(rule_line, TK_M_SUBSYSTEM, op, value, NULL); + } else if (streq(key, "DRIVER")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_DRIVER, op, value, NULL); + } else if (streq(key, "ATTR")) { + r = check_attr_format_and_warn(rules, key, attr); + if (r < 0) + return r; + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) { + log_token_warning(rules, "%s key takes '==', '!=', or '=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + check_value_format_and_warn(rules, key, value, false); + r = rule_line_add_token(rule_line, TK_A_ATTR, op, value, attr); + } else + r = rule_line_add_token(rule_line, TK_M_ATTR, op, value, attr); + } else if (streq(key, "SYSCTL")) { + r = check_attr_format_and_warn(rules, key, attr); + if (r < 0) + return r; + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) { + log_token_warning(rules, "%s key takes '==', '!=', or '=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (!is_match) { + check_value_format_and_warn(rules, key, value, false); + r = rule_line_add_token(rule_line, TK_A_SYSCTL, op, value, attr); + } else + r = rule_line_add_token(rule_line, TK_M_SYSCTL, op, value, attr); + } else if (streq(key, "KERNELS")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_KERNEL, op, value, NULL); + } else if (streq(key, "SUBSYSTEMS")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_SUBSYSTEM, op, value, NULL); + } else if (streq(key, "DRIVERS")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_DRIVER, op, value, NULL); + } else if (streq(key, "ATTRS")) { + r = check_attr_format_and_warn(rules, key, attr); + if (r < 0) + return r; + if (!is_match) + return log_token_invalid_op(rules, key); + + if (startswith(attr, "device/")) + log_token_warning(rules, "'device' link may not be available in future kernels; please fix it."); + if (strstr(attr, "../")) + log_token_warning(rules, "Direct reference to parent sysfs directory, may break in future kernels; please fix it."); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_ATTR, op, value, attr); + } else if (streq(key, "TAGS")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_PARENTS_TAG, op, value, NULL); + } else if (streq(key, "TEST")) { + mode_t mode = MODE_INVALID; + + if (!isempty(attr)) { + r = parse_mode(attr, &mode); + if (r < 0) + return log_token_error_errno(rules, r, "Failed to parse mode '%s': %m", attr); + } + check_value_format_and_warn(rules, key, value, true); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_TEST, op, value, MODE_TO_PTR(mode)); + } else if (streq(key, "PROGRAM")) { + if (attr) + return log_token_invalid_attr(rules, key); + check_value_format_and_warn(rules, key, value, true); + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (!is_match) + op = OP_MATCH; + + r = rule_line_add_token(rule_line, TK_M_PROGRAM, op, value, NULL); + } else if (streq(key, "IMPORT")) { + if (isempty(attr)) + return log_token_invalid_attr(rules, key); + check_value_format_and_warn(rules, key, value, true); + if (op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (!is_match) + op = OP_MATCH; + + if (streq(attr, "file")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_FILE, op, value, NULL); + else if (streq(attr, "program")) { + UdevBuiltinCommand cmd; + + cmd = udev_builtin_lookup(value); + if (cmd >= 0) { + log_token_debug(rules,"Found builtin command '%s' for %s, replacing attribute", value, key); + r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + } else + r = rule_line_add_token(rule_line, TK_M_IMPORT_PROGRAM, op, value, NULL); + } else if (streq(attr, "builtin")) { + UdevBuiltinCommand cmd; + + cmd = udev_builtin_lookup(value); + if (cmd < 0) + return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), + "Unknown builtin command: %s", value); + r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + } else if (streq(attr, "db")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_DB, op, value, NULL); + else if (streq(attr, "cmdline")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_CMDLINE, op, value, NULL); + else if (streq(attr, "parent")) + r = rule_line_add_token(rule_line, TK_M_IMPORT_PARENT, op, value, NULL); + else + return log_token_invalid_attr(rules, key); + } else if (streq(key, "RESULT")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (!is_match) + return log_token_invalid_op(rules, key); + + r = rule_line_add_token(rule_line, TK_M_RESULT, op, value, NULL); + } else if (streq(key, "OPTIONS")) { + char *tmp; + + if (attr) + return log_token_invalid_attr(rules, key); + if (is_match || op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ADD) + op = OP_ASSIGN; + + if (streq(value, "string_escape=none")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_NONE, op, NULL, NULL); + else if (streq(value, "string_escape=replace")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_REPLACE, op, NULL, NULL); + else if (streq(value, "db_persist")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_DB_PERSIST, op, NULL, NULL); + else if (streq(value, "watch")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(1)); + else if (streq(value, "nowatch")) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(0)); + else if ((tmp = startswith(value, "static_node="))) + r = rule_line_add_token(rule_line, TK_A_OPTIONS_STATIC_NODE, op, tmp, NULL); + else if ((tmp = startswith(value, "link_priority="))) { + int prio; + + r = safe_atoi(tmp, &prio); + if (r < 0) + return log_token_error_errno(rules, r, "Failed to parse link priority '%s': %m", tmp); + r = rule_line_add_token(rule_line, TK_A_OPTIONS_DEVLINK_PRIORITY, op, NULL, INT_TO_PTR(prio)); + } else { + log_token_warning(rules, "Invalid value for OPTIONS key, ignoring: '%s'", value); + return 0; + } + } else if (streq(key, "OWNER")) { + uid_t uid; + + if (attr) + return log_token_invalid_attr(rules, key); + if (is_match || op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ADD) { + log_token_warning(rules, "%s key takes '=' or ':=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (parse_uid(value, &uid) >= 0) + r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid)); + else if (rules->resolve_name_timing == RESOLVE_NAME_EARLY && + rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) { + r = rule_resolve_user(rules, value, &uid); + if (r < 0) + return log_token_error_errno(rules, r, "Failed to resolve user name '%s': %m", value); + + r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid)); + } else if (rules->resolve_name_timing != RESOLVE_NAME_NEVER) { + check_value_format_and_warn(rules, key, value, true); + r = rule_line_add_token(rule_line, TK_A_OWNER, op, value, NULL); + } else { + log_token_debug(rules, "User name resolution is disabled, ignoring %s=%s", key, value); + return 0; + } + } else if (streq(key, "GROUP")) { + gid_t gid; + + if (attr) + return log_token_invalid_attr(rules, key); + if (is_match || op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ADD) { + log_token_warning(rules, "%s key takes '=' or ':=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (parse_gid(value, &gid) >= 0) + r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid)); + else if (rules->resolve_name_timing == RESOLVE_NAME_EARLY && + rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) { + r = rule_resolve_group(rules, value, &gid); + if (r < 0) + return log_token_error_errno(rules, r, "Failed to resolve group name '%s': %m", value); + + r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid)); + } else if (rules->resolve_name_timing != RESOLVE_NAME_NEVER) { + check_value_format_and_warn(rules, key, value, true); + r = rule_line_add_token(rule_line, TK_A_GROUP, op, value, NULL); + } else { + log_token_debug(rules, "Resolving group name is disabled, ignoring %s=%s", key, value); + return 0; + } + } else if (streq(key, "MODE")) { + mode_t mode; + + if (attr) + return log_token_invalid_attr(rules, key); + if (is_match || op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ADD) { + log_token_warning(rules, "%s key takes '=' or ':=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + if (parse_mode(value, &mode) >= 0) + r = rule_line_add_token(rule_line, TK_A_MODE_ID, op, NULL, MODE_TO_PTR(mode)); + else { + check_value_format_and_warn(rules, key, value, true); + r = rule_line_add_token(rule_line, TK_A_MODE, op, value, NULL); + } + } else if (streq(key, "SECLABEL")) { + if (isempty(attr)) + return log_token_invalid_attr(rules, key); + check_value_format_and_warn(rules, key, value, true); + if (is_match || op == OP_REMOVE) + return log_token_invalid_op(rules, key); + if (op == OP_ASSIGN_FINAL) { + log_token_warning(rules, "%s key takes '=' or '+=' operator, assuming '=', but please fix it.", key); + op = OP_ASSIGN; + } + + r = rule_line_add_token(rule_line, TK_A_SECLABEL, op, value, attr); + } else if (streq(key, "RUN")) { + if (is_match || op == OP_REMOVE) + return log_token_invalid_op(rules, key); + check_value_format_and_warn(rules, key, value, true); + if (!attr || streq(attr, "program")) + r = rule_line_add_token(rule_line, TK_A_RUN_PROGRAM, op, value, NULL); + else if (streq(attr, "builtin")) { + UdevBuiltinCommand cmd; + + cmd = udev_builtin_lookup(value); + if (cmd < 0) + return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), + "Unknown builtin command '%s', ignoring", value); + r = rule_line_add_token(rule_line, TK_A_RUN_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd)); + } else + return log_token_invalid_attr(rules, key); + } else if (streq(key, "GOTO")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (op != OP_ASSIGN) + return log_token_invalid_op(rules, key); + if (FLAGS_SET(rule_line->type, LINE_HAS_GOTO)) { + log_token_warning(rules, "Contains multiple GOTO keys, ignoring GOTO=\"%s\".", value); + return 0; + } + + rule_line->goto_label = value; + SET_FLAG(rule_line->type, LINE_HAS_GOTO, true); + return 1; + } else if (streq(key, "LABEL")) { + if (attr) + return log_token_invalid_attr(rules, key); + if (op != OP_ASSIGN) + return log_token_invalid_op(rules, key); + + rule_line->label = value; + SET_FLAG(rule_line->type, LINE_HAS_LABEL, true); + return 1; + } else + return log_token_error_errno(rules, SYNTHETIC_ERRNO(EINVAL), "Invalid key '%s'", key); + if (r < 0) + return log_oom(); + + return 1; +} + +static UdevRuleOperatorType parse_operator(const char *op) { + assert(op); + + if (startswith(op, "==")) + return OP_MATCH; + if (startswith(op, "!=")) + return OP_NOMATCH; + if (startswith(op, "+=")) + return OP_ADD; + if (startswith(op, "-=")) + return OP_REMOVE; + if (startswith(op, "=")) + return OP_ASSIGN; + if (startswith(op, ":=")) + return OP_ASSIGN_FINAL; + + return _OP_TYPE_INVALID; +} + +static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOperatorType *ret_op, char **ret_value) { + char *key_begin, *key_end, *attr, *tmp; + UdevRuleOperatorType op; + int r; + + assert(line); + assert(*line); + assert(ret_key); + assert(ret_op); + assert(ret_value); + + key_begin = skip_leading_chars(*line, WHITESPACE ","); + + if (isempty(key_begin)) + return 0; + + for (key_end = key_begin; ; key_end++) { + if (key_end[0] == '\0') + return -EINVAL; + if (strchr(WHITESPACE "={", key_end[0])) + break; + if (strchr("+-!:", key_end[0]) && key_end[1] == '=') + break; + } + if (key_end[0] == '{') { + attr = key_end + 1; + tmp = strchr(attr, '}'); + if (!tmp) + return -EINVAL; + *tmp++ = '\0'; + } else { + attr = NULL; + tmp = key_end; + } + + tmp = skip_leading_chars(tmp, NULL); + op = parse_operator(tmp); + if (op < 0) + return -EINVAL; + + key_end[0] = '\0'; + + tmp += op == OP_ASSIGN ? 1 : 2; + tmp = skip_leading_chars(tmp, NULL); + r = udev_rule_parse_value(tmp, ret_value, line); + if (r < 0) + return r; + + *ret_key = key_begin; + *ret_attr = attr; + *ret_op = op; + return 1; +} + +static void sort_tokens(UdevRuleLine *rule_line) { + UdevRuleToken *head_old; + + assert(rule_line); + + head_old = TAKE_PTR(rule_line->tokens); + rule_line->current_token = NULL; + + while (!LIST_IS_EMPTY(head_old)) { + UdevRuleToken *t, *min_token = NULL; + + LIST_FOREACH(tokens, t, head_old) + if (!min_token || min_token->type > t->type) + min_token = t; + + LIST_REMOVE(tokens, head_old, min_token); + rule_line_append_token(rule_line, min_token); + } +} + +static int rule_add_line(UdevRules *rules, const char *line_str, unsigned line_nr) { + _cleanup_(udev_rule_line_freep) UdevRuleLine *rule_line = NULL; + _cleanup_free_ char *line = NULL; + UdevRuleFile *rule_file; + char *p; + int r; + + assert(rules); + assert(rules->current_file); + assert(line_str); + + rule_file = rules->current_file; + + if (isempty(line_str)) + return 0; + + /* We use memdup_suffix0() here, since we want to add a second NUL byte to the end, since possibly + * some parsers might turn this into a "nulstr", which requires an extra NUL at the end. */ + line = memdup_suffix0(line_str, strlen(line_str) + 1); + if (!line) + return log_oom(); + + rule_line = new(UdevRuleLine, 1); + if (!rule_line) + return log_oom(); + + *rule_line = (UdevRuleLine) { + .line = TAKE_PTR(line), + .line_number = line_nr, + .rule_file = rule_file, + }; + + if (rule_file->current_line) + LIST_APPEND(rule_lines, rule_file->current_line, rule_line); + else + LIST_APPEND(rule_lines, rule_file->rule_lines, rule_line); + + rule_file->current_line = rule_line; + + for (p = rule_line->line; !isempty(p); ) { + char *key, *attr, *value; + UdevRuleOperatorType op; + + r = parse_line(&p, &key, &attr, &op, &value); + if (r < 0) + return log_token_error_errno(rules, r, "Invalid key/value pair, ignoring."); + if (r == 0) + break; + + r = parse_token(rules, key, attr, op, value); + if (r < 0) + return r; + } + + if (rule_line->type == 0) { + log_token_warning(rules, "The line takes no effect, ignoring."); + return 0; + } + + sort_tokens(rule_line); + TAKE_PTR(rule_line); + return 0; +} + +static void rule_resolve_goto(UdevRuleFile *rule_file) { + UdevRuleLine *line, *line_next, *i; + + assert(rule_file); + + /* link GOTOs to LABEL rules in this file to be able to fast-forward */ + LIST_FOREACH_SAFE(rule_lines, line, line_next, rule_file->rule_lines) { + if (!FLAGS_SET(line->type, LINE_HAS_GOTO)) + continue; + + LIST_FOREACH_AFTER(rule_lines, i, line) + if (streq_ptr(i->label, line->goto_label)) { + line->goto_line = i; + break; + } + + if (!line->goto_line) { + log_error("%s:%u: GOTO=\"%s\" has no matching label, ignoring", + rule_file->filename, line->line_number, line->goto_label); + + SET_FLAG(line->type, LINE_HAS_GOTO, false); + line->goto_label = NULL; + + if ((line->type & ~LINE_HAS_LABEL) == 0) { + log_notice("%s:%u: The line takes no effect any more, dropping", + rule_file->filename, line->line_number); + if (line->type == LINE_HAS_LABEL) + udev_rule_line_clear_tokens(line); + else + udev_rule_line_free(line); + } + } + } +} + +int udev_rules_parse_file(UdevRules *rules, const char *filename) { + _cleanup_free_ char *continuation = NULL, *name = NULL; + _cleanup_fclose_ FILE *f = NULL; + UdevRuleFile *rule_file; + bool ignore_line = false; + unsigned line_nr = 0; + int r; + + f = fopen(filename, "re"); + if (!f) { + if (errno == ENOENT) + return 0; + + return -errno; + } + + (void) fd_warn_permissions(filename, fileno(f)); + + if (null_or_empty_fd(fileno(f))) { + log_debug("Skipping empty file: %s", filename); + return 0; + } + + log_debug("Reading rules file: %s", filename); + + name = strdup(filename); + if (!name) + return log_oom(); + + rule_file = new(UdevRuleFile, 1); + if (!rule_file) + return log_oom(); + + *rule_file = (UdevRuleFile) { + .filename = TAKE_PTR(name), + }; + + if (rules->current_file) + LIST_APPEND(rule_files, rules->current_file, rule_file); + else + LIST_APPEND(rule_files, rules->rule_files, rule_file); + + rules->current_file = rule_file; + + for (;;) { + _cleanup_free_ char *buf = NULL; + size_t len; + char *line; + + r = read_line(f, UTIL_LINE_SIZE, &buf); + if (r < 0) + return r; + if (r == 0) + break; + + line_nr++; + line = skip_leading_chars(buf, NULL); + + if (line[0] == '#') + continue; + + len = strlen(line); + + if (continuation && !ignore_line) { + if (strlen(continuation) + len >= UTIL_LINE_SIZE) + ignore_line = true; + + if (!strextend(&continuation, line, NULL)) + return log_oom(); + + if (!ignore_line) { + line = continuation; + len = strlen(line); + } + } + + if (len > 0 && line[len - 1] == '\\') { + if (ignore_line) + continue; + + line[len - 1] = '\0'; + if (!continuation) { + continuation = strdup(line); + if (!continuation) + return log_oom(); + } + + continue; + } + + if (ignore_line) + log_error("%s:%u: Line is too long, ignored", filename, line_nr); + else if (len > 0) + (void) rule_add_line(rules, line, line_nr); + + continuation = mfree(continuation); + ignore_line = false; + } + + rule_resolve_goto(rule_file); + return 0; +} + +UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing) { + assert(resolve_name_timing >= 0 && resolve_name_timing < _RESOLVE_NAME_TIMING_MAX); + + UdevRules *rules = new(UdevRules, 1); + if (!rules) + return NULL; + + *rules = (UdevRules) { + .resolve_name_timing = resolve_name_timing, + }; + + return rules; +} + +int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_strv_free_ char **files = NULL; + char **f; + int r; + + rules = udev_rules_new(resolve_name_timing); + if (!rules) + return -ENOMEM; + + (void) udev_rules_check_timestamp(rules); + + r = conf_files_list_strv(&files, ".rules", NULL, 0, RULES_DIRS); + if (r < 0) + return log_debug_errno(r, "Failed to enumerate rules files: %m"); + + STRV_FOREACH(f, files) { + r = udev_rules_parse_file(rules, *f); + if (r < 0) + log_debug_errno(r, "Failed to read rules file %s, ignoring: %m", *f); + } + + *ret_rules = TAKE_PTR(rules); + return 0; +} + +bool udev_rules_check_timestamp(UdevRules *rules) { + if (!rules) + return false; + + return paths_check_timestamp(RULES_DIRS, &rules->dirs_ts_usec, true); +} + +static bool token_match_string(UdevRuleToken *token, const char *str) { + const char *i, *value; + bool match = false; + + assert(token); + assert(token->value); + assert(token->type < _TK_M_MAX); + + str = strempty(str); + value = token->value; + + switch (token->match_type) { + case MATCH_TYPE_EMPTY: + match = isempty(str); + break; + case MATCH_TYPE_SUBSYSTEM: + match = STR_IN_SET(str, "subsystem", "class", "bus"); + break; + case MATCH_TYPE_PLAIN_WITH_EMPTY: + if (isempty(str)) { + match = true; + break; + } + _fallthrough_; + case MATCH_TYPE_PLAIN: + NULSTR_FOREACH(i, value) + if (streq(i, str)) { + match = true; + break; + } + break; + case MATCH_TYPE_GLOB_WITH_EMPTY: + if (isempty(str)) { + match = true; + break; + } + _fallthrough_; + case MATCH_TYPE_GLOB: + NULSTR_FOREACH(i, value) + if ((fnmatch(i, str, 0) == 0)) { + match = true; + break; + } + break; + default: + assert_not_reached("Invalid match type"); + } + + return token->op == (match ? OP_MATCH : OP_NOMATCH); +} + +static bool token_match_attr(UdevRuleToken *token, sd_device *dev, UdevEvent *event) { + char nbuf[UTIL_NAME_SIZE], vbuf[UTIL_NAME_SIZE]; + const char *name, *value; + + assert(token); + assert(dev); + assert(event); + + name = token->data; + + switch (token->attr_subst_type) { + case SUBST_TYPE_FORMAT: + (void) udev_event_apply_format(event, name, nbuf, sizeof(nbuf), false); + name = nbuf; + _fallthrough_; + case SUBST_TYPE_PLAIN: + if (sd_device_get_sysattr_value(dev, name, &value) < 0) + return false; + break; + case SUBST_TYPE_SUBSYS: + if (util_resolve_subsys_kernel(name, vbuf, sizeof(vbuf), true) < 0) + return false; + value = vbuf; + break; + default: + assert_not_reached("Invalid attribute substitution type"); + } + + /* remove trailing whitespace, if not asked to match for it */ + if (token->attr_match_remove_trailing_whitespace) { + if (value != vbuf) { + strscpy(vbuf, sizeof(vbuf), value); + value = vbuf; + } + + delete_trailing_chars(vbuf, NULL); + } + + return token_match_string(token, value); +} + +static int get_property_from_string(char *line, char **ret_key, char **ret_value) { + char *key, *val; + size_t len; + + assert(line); + assert(ret_key); + assert(ret_value); + + /* find key */ + key = skip_leading_chars(line, NULL); + + /* comment or empty line */ + if (IN_SET(key[0], '#', '\0')) { + *ret_key = *ret_value = NULL; + return 0; + } + + /* split key/value */ + val = strchr(key, '='); + if (!val) + return -EINVAL; + *val++ = '\0'; + + key = strstrip(key); + if (isempty(key)) + return -EINVAL; + + val = strstrip(val); + if (isempty(val)) + return -EINVAL; + + /* unquote */ + if (IN_SET(val[0], '"', '\'')) { + len = strlen(val); + if (len == 1 || val[len-1] != val[0]) + return -EINVAL; + val[len-1] = '\0'; + val++; + } + + *ret_key = key; + *ret_value = val; + return 1; +} + +static int import_parent_into_properties(sd_device *dev, const char *filter) { + const char *key, *val; + sd_device *parent; + int r; + + assert(dev); + assert(filter); + + r = sd_device_get_parent(dev, &parent); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + FOREACH_DEVICE_PROPERTY(parent, key, val) { + if (fnmatch(filter, key, 0) != 0) + continue; + r = device_add_property(dev, key, val); + if (r < 0) + return r; + } + + return 1; +} + +static int attr_subst_subdir(char attr[static UTIL_PATH_SIZE]) { + _cleanup_closedir_ DIR *dir = NULL; + struct dirent *dent; + char buf[UTIL_PATH_SIZE], *p; + const char *tail; + size_t len, size; + + assert(attr); + + tail = strstr(attr, "/*/"); + if (!tail) + return 0; + + len = tail - attr + 1; /* include slash at the end */ + tail += 2; /* include slash at the beginning */ + + p = buf; + size = sizeof(buf); + size -= strnpcpy(&p, size, attr, len); + + dir = opendir(buf); + if (!dir) + return -errno; + + FOREACH_DIRENT_ALL(dent, dir, break) { + if (dent->d_name[0] == '.') + continue; + + strscpyl(p, size, dent->d_name, tail, NULL); + if (faccessat(dirfd(dir), p, F_OK, 0) < 0) + continue; + + strcpy(attr, buf); + return 0; + } + + return -ENOENT; +} + +static int udev_rule_apply_token_to_event( + UdevRules *rules, + sd_device *dev, + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list) { + + UdevRuleToken *token; + char buf[UTIL_PATH_SIZE]; + const char *val; + size_t count; + bool match; + int r; + + assert(rules); + assert(dev); + assert(event); + + /* This returns the following values: + * 0 on the current token does not match the event, + * 1 on the current token matches the event, and + * negative errno on some critical errors. */ + + token = rules->current_file->current_line->current_token; + + switch (token->type) { + case TK_M_ACTION: { + DeviceAction a; + + r = device_get_action(dev, &a); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to get uevent action type: %m"); + + return token_match_string(token, device_action_to_string(a)); + } + case TK_M_DEVPATH: + r = sd_device_get_devpath(dev, &val); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to get devpath: %m"); + + return token_match_string(token, val); + case TK_M_KERNEL: + case TK_M_PARENTS_KERNEL: + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to get sysname: %m"); + + return token_match_string(token, val); + case TK_M_DEVLINK: + FOREACH_DEVICE_DEVLINK(dev, val) + if (token_match_string(token, strempty(startswith(val, "/dev/")))) + return token->op == OP_MATCH; + return token->op == OP_NOMATCH; + case TK_M_NAME: + return token_match_string(token, event->name); + case TK_M_ENV: + if (sd_device_get_property_value(dev, token->data, &val) < 0) + val = hashmap_get(properties_list, token->data); + + return token_match_string(token, val); + case TK_M_CONST: { + const char *k = token->data; + + if (streq(k, "arch")) + val = architecture_to_string(uname_architecture()); + else if (streq(k, "virt")) + val = virtualization_to_string(detect_virtualization()); + else + assert_not_reached("Invalid CONST key"); + return token_match_string(token, val); + } + case TK_M_TAG: + case TK_M_PARENTS_TAG: + FOREACH_DEVICE_TAG(dev, val) + if (token_match_string(token, val)) + return token->op == OP_MATCH; + return token->op == OP_NOMATCH; + case TK_M_SUBSYSTEM: + case TK_M_PARENTS_SUBSYSTEM: + r = sd_device_get_subsystem(dev, &val); + if (r == -ENOENT) + val = NULL; + else if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to get subsystem: %m"); + + return token_match_string(token, val); + case TK_M_DRIVER: + case TK_M_PARENTS_DRIVER: + r = sd_device_get_driver(dev, &val); + if (r == -ENOENT) + val = NULL; + else if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to get driver: %m"); + + return token_match_string(token, val); + case TK_M_ATTR: + case TK_M_PARENTS_ATTR: + return token_match_attr(token, dev, event); + case TK_M_SYSCTL: { + _cleanup_free_ char *value = NULL; + + (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false); + r = sysctl_read(sysctl_normalize(buf), &value); + if (r < 0 && r != -ENOENT) + return log_rule_error_errno(dev, rules, r, "Failed to read sysctl '%s': %m", buf); + + return token_match_string(token, strstrip(value)); + } + case TK_M_TEST: { + mode_t mode = PTR_TO_MODE(token->data); + struct stat statbuf; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + if (!path_is_absolute(buf) && + util_resolve_subsys_kernel(buf, buf, sizeof(buf), false) < 0) { + char tmp[UTIL_PATH_SIZE]; + + r = sd_device_get_syspath(dev, &val); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to get syspath: %m"); + + strscpy(tmp, sizeof(tmp), buf); + strscpyl(buf, sizeof(buf), val, "/", tmp, NULL); + } + + r = attr_subst_subdir(buf); + if (r == -ENOENT) + return token->op == OP_NOMATCH; + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to test for the existence of '%s': %m", buf); + + if (stat(buf, &statbuf) < 0) + return token->op == OP_NOMATCH; + + if (mode == MODE_INVALID) + return token->op == OP_MATCH; + + match = (statbuf.st_mode & mode) > 0; + return token->op == (match ? OP_MATCH : OP_NOMATCH); + } + case TK_M_PROGRAM: { + char result[UTIL_LINE_SIZE]; + + event->program_result = mfree(event->program_result); + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + log_rule_debug(dev, rules, "Running PROGRAM '%s'", buf); + + r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof(result)); + if (r != 0) { + if (r < 0) + log_rule_warning_errno(dev, rules, r, "Failed to execute \"%s\": %m", buf); + else /* returned value is positive when program fails */ + log_rule_debug(dev, rules, "Command \"%s\" returned %d (error)", buf, r); + return token->op == OP_NOMATCH; + } + + delete_trailing_chars(result, "\n"); + count = util_replace_chars(result, UDEV_ALLOWED_CHARS_INPUT); + if (count > 0) + log_rule_debug(dev, rules, "Replaced %zu character(s) in result of \"%s\"", + count, buf); + + event->program_result = strdup(result); + return token->op == OP_MATCH; + } + case TK_M_IMPORT_FILE: { + _cleanup_fclose_ FILE *f = NULL; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + log_rule_debug(dev, rules, "Importing properties from '%s'", buf); + + f = fopen(buf, "re"); + if (!f) { + if (errno != ENOENT) + return log_rule_error_errno(dev, rules, errno, + "Failed to open '%s': %m", buf); + return token->op == OP_NOMATCH; + } + + for (;;) { + _cleanup_free_ char *line = NULL; + char *key, *value; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) { + log_rule_debug_errno(dev, rules, r, + "Failed to read '%s', ignoring: %m", buf); + return token->op == OP_NOMATCH; + } + if (r == 0) + break; + + r = get_property_from_string(line, &key, &value); + if (r < 0) { + log_rule_debug_errno(dev, rules, r, + "Failed to parse key and value from '%s', ignoring: %m", + line); + continue; + } + if (r == 0) + continue; + + r = device_add_property(dev, key, value); + if (r < 0) + return log_rule_error_errno(dev, rules, r, + "Failed to add property %s=%s: %m", + key, value); + } + + return token->op == OP_MATCH; + } + case TK_M_IMPORT_PROGRAM: { + char result[UTIL_LINE_SIZE], *line, *pos; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + log_rule_debug(dev, rules, "Importing properties from results of '%s'", buf); + + r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof result); + if (r != 0) { + if (r < 0) + log_rule_warning_errno(dev, rules, r, "Failed to execute '%s', ignoring: %m", buf); + else /* returned value is positive when program fails */ + log_rule_debug(dev, rules, "Command \"%s\" returned %d (error), ignoring", buf, r); + return token->op == OP_NOMATCH; + } + + for (line = result; !isempty(line); line = pos) { + char *key, *value; + + pos = strchr(line, '\n'); + if (pos) + *pos++ = '\0'; + + r = get_property_from_string(line, &key, &value); + if (r < 0) { + log_rule_debug_errno(dev, rules, r, + "Failed to parse key and value from '%s', ignoring: %m", + line); + continue; + } + if (r == 0) + continue; + + r = device_add_property(dev, key, value); + if (r < 0) + return log_rule_error_errno(dev, rules, r, + "Failed to add property %s=%s: %m", + key, value); + } + + return token->op == OP_MATCH; + } + case TK_M_IMPORT_BUILTIN: { + UdevBuiltinCommand cmd = PTR_TO_UDEV_BUILTIN_CMD(token->data); + unsigned mask = 1U << (int) cmd; + + if (udev_builtin_run_once(cmd)) { + /* check if we ran already */ + if (event->builtin_run & mask) { + log_rule_debug(dev, rules, "Skipping builtin '%s' in IMPORT key", + udev_builtin_name(cmd)); + /* return the result from earlier run */ + return token->op == (event->builtin_ret & mask ? OP_NOMATCH : OP_MATCH); + } + /* mark as ran */ + event->builtin_run |= mask; + } + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + log_rule_debug(dev, rules, "Importing properties from results of builtin command '%s'", buf); + + r = udev_builtin_run(dev, cmd, buf, false); + if (r < 0) { + /* remember failure */ + log_rule_debug_errno(dev, rules, r, "Failed to run builtin '%s': %m", buf); + event->builtin_ret |= mask; + } + return token->op == (r >= 0 ? OP_MATCH : OP_NOMATCH); + } + case TK_M_IMPORT_DB: { + if (!event->dev_db_clone) + return token->op == OP_NOMATCH; + r = sd_device_get_property_value(event->dev_db_clone, token->value, &val); + if (r == -ENOENT) + return token->op == OP_NOMATCH; + if (r < 0) + return log_rule_error_errno(dev, rules, r, + "Failed to get property '%s' from database: %m", + token->value); + + r = device_add_property(dev, token->value, val); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to add property '%s=%s': %m", + token->value, val); + return token->op == OP_MATCH; + } + case TK_M_IMPORT_CMDLINE: { + _cleanup_free_ char *value = NULL; + + r = proc_cmdline_get_key(token->value, PROC_CMDLINE_VALUE_OPTIONAL|PROC_CMDLINE_IGNORE_EFI_OPTIONS, &value); + if (r < 0) + return log_rule_error_errno(dev, rules, r, + "Failed to read '%s' option from /proc/cmdline: %m", + token->value); + if (r == 0) + return token->op == OP_NOMATCH; + + r = device_add_property(dev, token->value, value ?: "1"); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to add property '%s=%s': %m", + token->value, value ?: "1"); + return token->op == OP_MATCH; + } + case TK_M_IMPORT_PARENT: { + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + r = import_parent_into_properties(dev, buf); + if (r < 0) + return log_rule_error_errno(dev, rules, r, + "Failed to import properties '%s' from parent: %m", + buf); + return token->op == (r > 0 ? OP_MATCH : OP_NOMATCH); + } + case TK_M_RESULT: + return token_match_string(token, event->program_result); + case TK_A_OPTIONS_STRING_ESCAPE_NONE: + event->esc = ESCAPE_NONE; + break; + case TK_A_OPTIONS_STRING_ESCAPE_REPLACE: + event->esc = ESCAPE_REPLACE; + break; + case TK_A_OPTIONS_DB_PERSIST: + device_set_db_persist(dev); + break; + case TK_A_OPTIONS_INOTIFY_WATCH: + if (event->inotify_watch_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->inotify_watch_final = true; + + event->inotify_watch = token->data; + break; + case TK_A_OPTIONS_DEVLINK_PRIORITY: + device_set_devlink_priority(dev, PTR_TO_INT(token->data)); + break; + case TK_A_OWNER: { + char owner[UTIL_NAME_SIZE]; + const char *ow = owner; + + if (event->owner_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->owner_final = true; + + (void) udev_event_apply_format(event, token->value, owner, sizeof(owner), false); + r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) + log_unknown_owner(dev, rules, r, "user", owner); + else + log_rule_debug(dev, rules, "OWNER %s(%u)", owner, event->uid); + break; + } + case TK_A_GROUP: { + char group[UTIL_NAME_SIZE]; + const char *gr = group; + + if (event->group_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->group_final = true; + + (void) udev_event_apply_format(event, token->value, group, sizeof(group), false); + r = get_group_creds(&gr, &event->gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) + log_unknown_owner(dev, rules, r, "group", group); + else + log_rule_debug(dev, rules, "GROUP %s(%u)", group, event->gid); + break; + } + case TK_A_MODE: { + char mode_str[UTIL_NAME_SIZE]; + + if (event->mode_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->mode_final = true; + + (void) udev_event_apply_format(event, token->value, mode_str, sizeof(mode_str), false); + r = parse_mode(mode_str, &event->mode); + if (r < 0) + log_rule_error_errno(dev, rules, r, "Failed to parse mode '%s', ignoring: %m", mode_str); + else + log_rule_debug(dev, rules, "MODE %#o", event->mode); + break; + } + case TK_A_OWNER_ID: + if (event->owner_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->owner_final = true; + if (!token->data) + break; + event->uid = PTR_TO_UID(token->data); + log_rule_debug(dev, rules, "OWNER %u", event->uid); + break; + case TK_A_GROUP_ID: + if (event->group_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->group_final = true; + if (!token->data) + break; + event->gid = PTR_TO_GID(token->data); + log_rule_debug(dev, rules, "GROUP %u", event->gid); + break; + case TK_A_MODE_ID: + if (event->mode_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->mode_final = true; + if (!token->data) + break; + event->mode = PTR_TO_MODE(token->data); + log_rule_debug(dev, rules, "MODE %#o", event->mode); + break; + case TK_A_SECLABEL: { + _cleanup_free_ char *name = NULL, *label = NULL; + char label_str[UTIL_LINE_SIZE] = {}; + + name = strdup(token->data); + if (!name) + return log_oom(); + + (void) udev_event_apply_format(event, token->value, label_str, sizeof(label_str), false); + if (!isempty(label_str)) + label = strdup(label_str); + else + label = strdup(token->value); + if (!label) + return log_oom(); + + if (token->op == OP_ASSIGN) + ordered_hashmap_clear_free_free(event->seclabel_list); + + r = ordered_hashmap_ensure_allocated(&event->seclabel_list, NULL); + if (r < 0) + return log_oom(); + + r = ordered_hashmap_put(event->seclabel_list, name, label); + if (r < 0) + return log_oom(); + log_rule_debug(dev, rules, "SECLABEL{%s}='%s'", name, label); + name = label = NULL; + break; + } + case TK_A_ENV: { + const char *name = token->data; + char value_new[UTIL_NAME_SIZE], *p = value_new; + size_t l = sizeof(value_new); + + if (isempty(token->value)) { + if (token->op == OP_ADD) + break; + r = device_add_property(dev, name, NULL); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to remove property '%s': %m", name); + break; + } + + if (token->op == OP_ADD && + sd_device_get_property_value(dev, name, &val) >= 0) + l = strpcpyl(&p, l, val, " ", NULL); + + (void) udev_event_apply_format(event, token->value, p, l, false); + + r = device_add_property(dev, name, value_new); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to add property '%s=%s': %m", name, value_new); + break; + } + case TK_A_TAG: { + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + if (token->op == OP_ASSIGN) + device_cleanup_tags(dev); + + if (buf[strspn(buf, ALPHANUMERICAL "-_")] != '\0') { + log_rule_error(dev, rules, "Invalid tag name '%s', ignoring", buf); + break; + } + if (token->op == OP_REMOVE) + device_remove_tag(dev, buf); + else { + r = device_add_tag(dev, buf, true); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to add tag '%s': %m", buf); + } + break; + } + case TK_A_NAME: { + if (event->name_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->name_final = true; + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + if (IN_SET(event->esc, ESCAPE_UNSET, ESCAPE_REPLACE)) { + count = util_replace_chars(buf, "/"); + if (count > 0) + log_rule_debug(dev, rules, "Replaced %zu character(s) from result of NAME=\"%s\"", + count, token->value); + } + if (sd_device_get_devnum(dev, NULL) >= 0 && + (sd_device_get_devname(dev, &val) < 0 || + !streq_ptr(buf, path_startswith(val, "/dev/")))) { + log_rule_error(dev, rules, + "Kernel device nodes cannot be renamed, ignoring NAME=\"%s\"; please fix it.", + token->value); + break; + } + if (free_and_strdup(&event->name, buf) < 0) + return log_oom(); + + log_rule_debug(dev, rules, "NAME '%s'", event->name); + break; + } + case TK_A_DEVLINK: { + char *p; + + if (event->devlink_final) + break; + if (sd_device_get_devnum(dev, NULL) < 0) + break; + if (token->op == OP_ASSIGN_FINAL) + event->devlink_final = true; + if (IN_SET(token->op, OP_ASSIGN, OP_ASSIGN_FINAL)) + device_cleanup_devlinks(dev); + + /* allow multiple symlinks separated by spaces */ + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), event->esc != ESCAPE_NONE); + if (event->esc == ESCAPE_UNSET) + count = util_replace_chars(buf, "/ "); + else if (event->esc == ESCAPE_REPLACE) + count = util_replace_chars(buf, "/"); + else + count = 0; + if (count > 0) + log_rule_debug(dev, rules, "Replaced %zu character(s) from result of LINK", count); + + p = skip_leading_chars(buf, NULL); + while (!isempty(p)) { + char filename[UTIL_PATH_SIZE], *next; + + next = strchr(p, ' '); + if (next) { + *next++ = '\0'; + next = skip_leading_chars(next, NULL); + } + + strscpyl(filename, sizeof(filename), "/dev/", p, NULL); + r = device_add_devlink(dev, filename); + if (r < 0) + return log_rule_error_errno(dev, rules, r, "Failed to add devlink '%s': %m", filename); + + log_rule_debug(dev, rules, "LINK '%s'", p); + p = next; + } + break; + } + case TK_A_ATTR: { + const char *key_name = token->data; + char value[UTIL_NAME_SIZE]; + + if (util_resolve_subsys_kernel(key_name, buf, sizeof(buf), false) < 0 && + sd_device_get_syspath(dev, &val) >= 0) + strscpyl(buf, sizeof(buf), val, "/", key_name, NULL); + + r = attr_subst_subdir(buf); + if (r < 0) { + log_rule_error_errno(dev, rules, r, "Could not find file matches '%s', ignoring: %m", buf); + break; + } + (void) udev_event_apply_format(event, token->value, value, sizeof(value), false); + + log_rule_debug(dev, rules, "ATTR '%s' writing '%s'", buf, value); + r = write_string_file(buf, value, WRITE_STRING_FILE_VERIFY_ON_FAILURE | WRITE_STRING_FILE_DISABLE_BUFFER | WRITE_STRING_FILE_AVOID_NEWLINE); + if (r < 0) + log_rule_error_errno(dev, rules, r, "Failed to write ATTR{%s}, ignoring: %m", buf); + break; + } + case TK_A_SYSCTL: { + char value[UTIL_NAME_SIZE]; + + (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false); + (void) udev_event_apply_format(event, token->value, value, sizeof(value), false); + sysctl_normalize(buf); + log_rule_debug(dev, rules, "SYSCTL '%s' writing '%s'", buf, value); + r = sysctl_write(buf, value); + if (r < 0) + log_rule_error_errno(dev, rules, r, "Failed to write SYSCTL{%s}='%s', ignoring: %m", buf, value); + break; + } + case TK_A_RUN_BUILTIN: + case TK_A_RUN_PROGRAM: { + _cleanup_free_ char *cmd = NULL; + + if (event->run_final) + break; + if (token->op == OP_ASSIGN_FINAL) + event->run_final = true; + + if (IN_SET(token->op, OP_ASSIGN, OP_ASSIGN_FINAL)) + ordered_hashmap_clear_free_key(event->run_list); + + r = ordered_hashmap_ensure_allocated(&event->run_list, NULL); + if (r < 0) + return log_oom(); + + (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false); + + cmd = strdup(buf); + if (!cmd) + return log_oom(); + + r = ordered_hashmap_put(event->run_list, cmd, token->data); + if (r < 0) + return log_oom(); + + TAKE_PTR(cmd); + + log_rule_debug(dev, rules, "RUN '%s'", token->value); + break; + } + case TK_A_OPTIONS_STATIC_NODE: + /* do nothing for events. */ + break; + default: + assert_not_reached("Invalid token type"); + } + + return true; +} + +static bool token_is_for_parents(UdevRuleToken *token) { + return token->type >= TK_M_PARENTS_KERNEL && token->type <= TK_M_PARENTS_TAG; +} + +static int udev_rule_apply_parent_token_to_event( + UdevRules *rules, + UdevEvent *event, + int timeout_signal) { + + UdevRuleLine *line; + UdevRuleToken *head; + int r; + + line = rules->current_file->current_line; + head = rules->current_file->current_line->current_token; + event->dev_parent = event->dev; + for (;;) { + LIST_FOREACH(tokens, line->current_token, head) { + if (!token_is_for_parents(line->current_token)) + return true; /* All parent tokens match. */ + r = udev_rule_apply_token_to_event(rules, event->dev_parent, event, 0, timeout_signal, NULL); + if (r < 0) + return r; + if (r == 0) + break; + } + if (!line->current_token) + /* All parent tokens match. But no assign tokens in the line. Hmm... */ + return true; + + if (sd_device_get_parent(event->dev_parent, &event->dev_parent) < 0) { + event->dev_parent = NULL; + return false; + } + } +} + +static int udev_rule_apply_line_to_event( + UdevRules *rules, + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list, + UdevRuleLine **next_line) { + + UdevRuleLine *line = rules->current_file->current_line; + UdevRuleLineType mask = LINE_HAS_GOTO | LINE_UPDATE_SOMETHING; + UdevRuleToken *token, *next_token; + bool parents_done = false; + DeviceAction action; + int r; + + r = device_get_action(event->dev, &action); + if (r < 0) + return r; + + if (action != DEVICE_ACTION_REMOVE) { + if (sd_device_get_devnum(event->dev, NULL) >= 0) + mask |= LINE_HAS_DEVLINK; + + if (sd_device_get_ifindex(event->dev, NULL) >= 0) + mask |= LINE_HAS_NAME; + } + + if ((line->type & mask) == 0) + return 0; + + event->esc = ESCAPE_UNSET; + LIST_FOREACH_SAFE(tokens, token, next_token, line->tokens) { + line->current_token = token; + + if (token_is_for_parents(token)) { + if (parents_done) + continue; + + r = udev_rule_apply_parent_token_to_event(rules, event, timeout_signal); + if (r <= 0) + return r; + + parents_done = true; + continue; + } + + r = udev_rule_apply_token_to_event(rules, event->dev, event, timeout_usec, timeout_signal, properties_list); + if (r <= 0) + return r; + } + + if (line->goto_line) + *next_line = line->goto_line; + + return 0; +} + +int udev_rules_apply_to_event( + UdevRules *rules, + UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list) { + + UdevRuleFile *file; + UdevRuleLine *next_line; + int r; + + assert(rules); + assert(event); + + LIST_FOREACH(rule_files, file, rules->rule_files) { + rules->current_file = file; + LIST_FOREACH_SAFE(rule_lines, file->current_line, next_line, file->rule_lines) { + r = udev_rule_apply_line_to_event(rules, event, timeout_usec, timeout_signal, properties_list, &next_line); + if (r < 0) + return r; + } + } + + return 0; +} + +static int apply_static_dev_perms(const char *devnode, uid_t uid, gid_t gid, mode_t mode, char **tags) { + char device_node[UTIL_PATH_SIZE], tags_dir[UTIL_PATH_SIZE], tag_symlink[UTIL_PATH_SIZE]; + _cleanup_free_ char *unescaped_filename = NULL; + struct stat stats; + char **t; + int r; + + assert(devnode); + + if (uid == UID_INVALID && gid == GID_INVALID && mode == MODE_INVALID && !tags) + return 0; + + strscpyl(device_node, sizeof(device_node), "/dev/", devnode, NULL); + if (stat(device_node, &stats) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to stat %s: %m", device_node); + return 0; + } + + if (!S_ISBLK(stats.st_mode) && !S_ISCHR(stats.st_mode)) { + log_warning("%s is neither block nor character device, ignoring.", device_node); + return 0; + } + + if (!strv_isempty(tags)) { + unescaped_filename = xescape(devnode, "/."); + if (!unescaped_filename) + return log_oom(); + } + + /* export the tags to a directory as symlinks, allowing otherwise dead nodes to be tagged */ + STRV_FOREACH(t, tags) { + strscpyl(tags_dir, sizeof(tags_dir), "/run/udev/static_node-tags/", *t, "/", NULL); + r = mkdir_p(tags_dir, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create %s: %m", tags_dir); + + strscpyl(tag_symlink, sizeof(tag_symlink), tags_dir, unescaped_filename, NULL); + r = symlink(device_node, tag_symlink); + if (r < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create symlink %s -> %s: %m", + tag_symlink, device_node); + } + + /* don't touch the permissions if only the tags were set */ + if (uid == UID_INVALID && gid == GID_INVALID && mode == MODE_INVALID) + return 0; + + if (mode == MODE_INVALID) + mode = gid_is_valid(gid) ? 0660 : 0600; + if (!uid_is_valid(uid)) + uid = 0; + if (!gid_is_valid(gid)) + gid = 0; + + r = chmod_and_chown(device_node, mode, uid, gid); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to chown '%s' %u %u: %m", device_node, uid, gid); + else + log_debug("chown '%s' %u:%u with mode %#o", device_node, uid, gid, mode); + + (void) utimensat(AT_FDCWD, device_node, NULL, 0); + return 0; +} + +static int udev_rule_line_apply_static_dev_perms(UdevRuleLine *rule_line) { + UdevRuleToken *token; + _cleanup_strv_free_ char **tags = NULL; + uid_t uid = UID_INVALID; + gid_t gid = GID_INVALID; + mode_t mode = MODE_INVALID; + int r; + + assert(rule_line); + + if (!FLAGS_SET(rule_line->type, LINE_HAS_STATIC_NODE)) + return 0; + + LIST_FOREACH(tokens, token, rule_line->tokens) + if (token->type == TK_A_OWNER_ID) + uid = PTR_TO_UID(token->data); + else if (token->type == TK_A_GROUP_ID) + gid = PTR_TO_GID(token->data); + else if (token->type == TK_A_MODE_ID) + mode = PTR_TO_MODE(token->data); + else if (token->type == TK_A_TAG) { + r = strv_extend(&tags, token->value); + if (r < 0) + return log_oom(); + } else if (token->type == TK_A_OPTIONS_STATIC_NODE) { + r = apply_static_dev_perms(token->value, uid, gid, mode, tags); + if (r < 0) + return r; + } + + return 0; +} + +int udev_rules_apply_static_dev_perms(UdevRules *rules) { + UdevRuleFile *file; + UdevRuleLine *line; + int r; + + assert(rules); + + LIST_FOREACH(rule_files, file, rules->rule_files) + LIST_FOREACH(rule_lines, line, file->rule_lines) { + r = udev_rule_line_apply_static_dev_perms(line); + if (r < 0) + return r; + } + + return 0; +} diff --git a/src/udev/udev-rules.h b/src/udev/udev-rules.h new file mode 100644 index 0000000..3f40a53 --- /dev/null +++ b/src/udev/udev-rules.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "hashmap.h" +#include "time-util.h" +#include "udev-util.h" + +typedef struct UdevRules UdevRules; +typedef struct UdevEvent UdevEvent; + +typedef enum { + ESCAPE_UNSET, + ESCAPE_NONE, /* OPTIONS="string_escape=none" */ + ESCAPE_REPLACE, /* OPTIONS="string_escape=replace" */ + _ESCAPE_TYPE_MAX, + _ESCAPE_TYPE_INVALID = -1 +} UdevRuleEscapeType; + +int udev_rules_parse_file(UdevRules *rules, const char *filename); +UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing); +int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing); +UdevRules *udev_rules_free(UdevRules *rules); +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRules*, udev_rules_free); + +bool udev_rules_check_timestamp(UdevRules *rules); +int udev_rules_apply_to_event(UdevRules *rules, UdevEvent *event, + usec_t timeout_usec, + int timeout_signal, + Hashmap *properties_list); +int udev_rules_apply_static_dev_perms(UdevRules *rules); diff --git a/src/udev/udev-watch.c b/src/udev/udev-watch.c new file mode 100644 index 0000000..8656fb0 --- /dev/null +++ b/src/udev/udev-watch.c @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2009 Canonical Ltd. + * Copyright © 2009 Scott James Remnant <scott@netsplit.com> + */ + +#include <sys/inotify.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "stdio-util.h" +#include "udev-watch.h" + +static int inotify_fd = -1; + +/* inotify descriptor, will be shared with rules directory; + * set to cloexec since we need our children to be able to add + * watches for us. */ +int udev_watch_init(void) { + inotify_fd = inotify_init1(IN_CLOEXEC); + if (inotify_fd < 0) + return -errno; + + return inotify_fd; +} + +/* Move any old watches directory out of the way, and then restore the watches. */ +int udev_watch_restore(void) { + struct dirent *ent; + DIR *dir; + int r; + + if (inotify_fd < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid inotify descriptor."); + + if (rename("/run/udev/watch", "/run/udev/watch.old") < 0) { + if (errno != ENOENT) + return log_warning_errno(errno, "Failed to move watches directory /run/udev/watch. Old watches will not be restored: %m"); + + return 0; + } + + dir = opendir("/run/udev/watch.old"); + if (!dir) + return log_warning_errno(errno, "Failed to open old watches directory /run/udev/watch.old. Old watches will not be restored: %m"); + + FOREACH_DIRENT_ALL(ent, dir, break) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + _cleanup_free_ char *device = NULL; + + if (ent->d_name[0] == '.') + continue; + + r = readlinkat_malloc(dirfd(dir), ent->d_name, &device); + if (r < 0) { + log_debug_errno(r, "Failed to read link '/run/udev/watch.old/%s', ignoring: %m", ent->d_name); + goto unlink; + } + + r = sd_device_new_from_device_id(&dev, device); + if (r < 0) { + log_debug_errno(r, "Failed to create sd_device object for '%s', ignoring: %m", device); + goto unlink; + } + + log_device_debug(dev, "Restoring old watch"); + (void) udev_watch_begin(dev); +unlink: + (void) unlinkat(dirfd(dir), ent->d_name, 0); + } + + (void) closedir(dir); + (void) rmdir("/run/udev/watch.old"); + + return 0; +} + +int udev_watch_begin(sd_device *dev) { + char filename[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)]; + const char *devnode, *id_filename; + int wd, r; + + if (inotify_fd < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid inotify descriptor."); + + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get device name: %m"); + + log_device_debug(dev, "Adding watch on '%s'", devnode); + wd = inotify_add_watch(inotify_fd, devnode, IN_CLOSE_WRITE); + if (wd < 0) + return log_device_full_errno(dev, errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to add device '%s' to watch: %m", devnode); + + device_set_watch_handle(dev, wd); + + xsprintf(filename, "/run/udev/watch/%d", wd); + r = mkdir_parents(filename, 0755); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to create parent directory of '%s': %m", filename); + (void) unlink(filename); + + r = device_get_id_filename(dev, &id_filename); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get device id-filename: %m"); + + if (symlink(id_filename, filename) < 0) + return log_device_error_errno(dev, errno, "Failed to create symlink %s: %m", filename); + + return 0; +} + +int udev_watch_end(sd_device *dev) { + char filename[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)]; + int wd, r; + + if (inotify_fd < 0) + return 0; /* Nothing to do. */ + + r = device_get_watch_handle(dev, &wd); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get watch handle, ignoring: %m"); + + log_device_debug(dev, "Removing watch"); + (void) inotify_rm_watch(inotify_fd, wd); + + xsprintf(filename, "/run/udev/watch/%d", wd); + (void) unlink(filename); + + device_set_watch_handle(dev, -1); + + return 0; +} + +int udev_watch_lookup(int wd, sd_device **ret) { + char filename[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)]; + _cleanup_free_ char *device = NULL; + int r; + + assert(ret); + + if (inotify_fd < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid inotify descriptor."); + + if (wd < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid watch handle."); + + xsprintf(filename, "/run/udev/watch/%d", wd); + r = readlink_malloc(filename, &device); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_debug_errno(r, "Failed to read link '%s': %m", filename); + + r = sd_device_new_from_device_id(ret, device); + if (r == -ENODEV) + return 0; + if (r < 0) + return log_debug_errno(r, "Failed to create sd_device object for '%s': %m", device); + + return 1; +} diff --git a/src/udev/udev-watch.h b/src/udev/udev-watch.h new file mode 100644 index 0000000..a15fa27 --- /dev/null +++ b/src/udev/udev-watch.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "sd-device.h" + +int udev_watch_init(void); +int udev_watch_restore(void); +int udev_watch_begin(sd_device *dev); +int udev_watch_end(sd_device *dev); +int udev_watch_lookup(int wd, sd_device **ret); diff --git a/src/udev/udev.conf b/src/udev/udev.conf new file mode 100644 index 0000000..07d7f0c --- /dev/null +++ b/src/udev/udev.conf @@ -0,0 +1,11 @@ +# see udev.conf(5) for details +# +# udevd is also started in the initrd. When this file is modified you might +# also want to rebuild the initrd, so that it will include the modified configuration. + +#udev_log=info +#children_max= +#exec_delay= +#event_timeout=180 +#timeout_signal=SIGKILL +#resolve_names=early diff --git a/src/udev/udev.pc.in b/src/udev/udev.pc.in new file mode 100644 index 0000000..7b4f400 --- /dev/null +++ b/src/udev/udev.pc.in @@ -0,0 +1,6 @@ +Name: udev +Description: udev +Version: @PROJECT_VERSION@ + +udev_dir=@udevlibexecdir@ +udevdir=${udev_dir} diff --git a/src/udev/udevadm-control.c b/src/udev/udevadm-control.c new file mode 100644 index 0000000..ef23a6c --- /dev/null +++ b/src/udev/udevadm-control.c @@ -0,0 +1,186 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "parse-util.h" +#include "process-util.h" +#include "syslog-util.h" +#include "time-util.h" +#include "udevadm.h" +#include "udev-ctrl.h" +#include "util.h" +#include "virt.h" + +static int help(void) { + printf("%s control OPTION\n\n" + "Control the udev daemon.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -e --exit Instruct the daemon to cleanup and exit\n" + " -l --log-level=LEVEL Set the udev log level for the daemon\n" + " -s --stop-exec-queue Do not execute events, queue only\n" + " -S --start-exec-queue Execute events, flush queue\n" + " -R --reload Reload rules and databases\n" + " -p --property=KEY=VALUE Set a global property for all events\n" + " -m --children-max=N Maximum number of children\n" + " --ping Wait for udev to respond to a ping message\n" + " -t --timeout=SECONDS Maximum time to block for a reply\n" + , program_invocation_short_name); + + return 0; +} + +int control_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *uctrl = NULL; + usec_t timeout = 60 * USEC_PER_SEC; + int c, r; + + enum { + ARG_PING = 0x100, + }; + + static const struct option options[] = { + { "exit", no_argument, NULL, 'e' }, + { "log-level", required_argument, NULL, 'l' }, + { "log-priority", required_argument, NULL, 'l' }, /* for backward compatibility */ + { "stop-exec-queue", no_argument, NULL, 's' }, + { "start-exec-queue", no_argument, NULL, 'S' }, + { "reload", no_argument, NULL, 'R' }, + { "reload-rules", no_argument, NULL, 'R' }, /* alias for -R */ + { "property", required_argument, NULL, 'p' }, + { "env", required_argument, NULL, 'p' }, /* alias for -p */ + { "children-max", required_argument, NULL, 'm' }, + { "ping", no_argument, NULL, ARG_PING }, + { "timeout", required_argument, NULL, 't' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + if (argc <= 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "This command expects one or more options."); + + r = udev_ctrl_new(&uctrl); + if (r < 0) + return log_error_errno(r, "Failed to initialize udev control: %m"); + + while ((c = getopt_long(argc, argv, "el:sSRp:m:t:Vh", options, NULL)) >= 0) + switch (c) { + case 'e': + r = udev_ctrl_send_exit(uctrl); + if (r == -ENOANO) + log_warning("Cannot specify --exit after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send exit request: %m"); + break; + case 'l': + r = log_level_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse log level '%s': %m", optarg); + + r = udev_ctrl_send_set_log_level(uctrl, r); + if (r == -ENOANO) + log_warning("Cannot specify --log-level after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send request to set log level: %m"); + break; + case 's': + r = udev_ctrl_send_stop_exec_queue(uctrl); + if (r == -ENOANO) + log_warning("Cannot specify --stop-exec-queue after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send request to stop exec queue: %m"); + break; + case 'S': + r = udev_ctrl_send_start_exec_queue(uctrl); + if (r == -ENOANO) + log_warning("Cannot specify --start-exec-queue after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send request to start exec queue: %m"); + break; + case 'R': + r = udev_ctrl_send_reload(uctrl); + if (r == -ENOANO) + log_warning("Cannot specify --reload after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send reload request: %m"); + break; + case 'p': + if (!strchr(optarg, '=')) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "expect <KEY>=<value> instead of '%s'", optarg); + + r = udev_ctrl_send_set_env(uctrl, optarg); + if (r == -ENOANO) + log_warning("Cannot specify --property after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send request to update environment: %m"); + break; + case 'm': { + unsigned i; + + r = safe_atou(optarg, &i); + if (r < 0) + return log_error_errno(r, "Failed to parse maximum number of events '%s': %m", optarg); + + r = udev_ctrl_send_set_children_max(uctrl, i); + if (r == -ENOANO) + log_warning("Cannot specify --children-max after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send request to set number of children: %m"); + break; + } + case ARG_PING: + r = udev_ctrl_send_ping(uctrl); + if (r == -ENOANO) + log_error("Cannot specify --ping after --exit, ignoring."); + else if (r < 0) + return log_error_errno(r, "Failed to send a ping message: %m"); + break; + case 't': + r = parse_sec(optarg, &timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg); + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option."); + } + + if (optind < argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Extraneous argument: %s", argv[optind]); + + r = udev_ctrl_wait(uctrl, timeout); + if (r < 0) + return log_error_errno(r, "Failed to wait for daemon to reply: %m"); + + return 0; +} diff --git a/src/udev/udevadm-hwdb.c b/src/udev/udevadm-hwdb.c new file mode 100644 index 0000000..3d21922 --- /dev/null +++ b/src/udev/udevadm-hwdb.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> + +#include "hwdb-util.h" +#include "udevadm.h" +#include "util.h" + +static const char *arg_test = NULL; +static const char *arg_root = NULL; +static const char *arg_hwdb_bin_dir = NULL; +static bool arg_update = false; +static bool arg_strict = false; + +static int help(void) { + printf("%s hwdb [OPTIONS]\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -u --update Update the hardware database\n" + " -s --strict When updating, return non-zero exit value on any parsing error\n" + " --usr Generate in " UDEVLIBEXECDIR " instead of /etc/udev\n" + " -t --test=MODALIAS Query database and print result\n" + " -r --root=PATH Alternative root path in the filesystem\n\n" + "NOTE:\n" + "The sub-command 'hwdb' is deprecated, and is left for backwards compatibility.\n" + "Please use systemd-hwdb instead.\n" + , program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_USR = 0x100, + }; + + static const struct option options[] = { + { "update", no_argument, NULL, 'u' }, + { "usr", no_argument, NULL, ARG_USR }, + { "strict", no_argument, NULL, 's' }, + { "test", required_argument, NULL, 't' }, + { "root", required_argument, NULL, 'r' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int c; + + while ((c = getopt_long(argc, argv, "ust:r:Vh", options, NULL)) >= 0) + switch(c) { + case 'u': + arg_update = true; + break; + case ARG_USR: + arg_hwdb_bin_dir = UDEVLIBEXECDIR; + break; + case 's': + arg_strict = true; + break; + case 't': + arg_test = optarg; + break; + case 'r': + arg_root = optarg; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option"); + } + + return 1; +} + +int hwdb_main(int argc, char *argv[], void *userdata) { + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (!arg_update && !arg_test) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Either --update or --test must be used."); + + if (arg_update) { + r = hwdb_update(arg_root, arg_hwdb_bin_dir, arg_strict, true); + if (r < 0) + return r; + } + + if (arg_test) + return hwdb_query(arg_test); + + return 0; +} diff --git a/src/udev/udevadm-info.c b/src/udev/udevadm-info.c new file mode 100644 index 0000000..5ff6256 --- /dev/null +++ b/src/udev/udevadm-info.c @@ -0,0 +1,519 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "sd-device.h" + +#include "alloc-util.h" +#include "device-enumerator-private.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "sort-util.h" +#include "string-table.h" +#include "string-util.h" +#include "udev-util.h" +#include "udevadm-util.h" +#include "udevadm.h" + +typedef enum ActionType { + ACTION_QUERY, + ACTION_ATTRIBUTE_WALK, + ACTION_DEVICE_ID_FILE, +} ActionType; + +typedef enum QueryType { + QUERY_NAME, + QUERY_PATH, + QUERY_SYMLINK, + QUERY_PROPERTY, + QUERY_ALL, +} QueryType; + +static bool arg_root = false; +static bool arg_export = false; +static const char *arg_export_prefix = NULL; +static usec_t arg_wait_for_initialization_timeout = 0; + +static bool skip_attribute(const char *name) { + /* Those are either displayed separately or should not be shown at all. */ + return STR_IN_SET(name, + "uevent", + "dev", + "modalias", + "resource", + "driver", + "subsystem", + "module"); +} + +typedef struct SysAttr { + const char *name; + const char *value; +} SysAttr; + +static int sysattr_compare(const SysAttr *a, const SysAttr *b) { + return strcmp(a->name, b->name); +} + +static int print_all_attributes(sd_device *device, bool is_parent) { + _cleanup_free_ SysAttr *sysattrs = NULL; + size_t n_items = 0, n_allocated = 0; + const char *name, *value; + + value = NULL; + (void) sd_device_get_devpath(device, &value); + printf(" looking at %sdevice '%s':\n", is_parent ? "parent " : "", strempty(value)); + + value = NULL; + (void) sd_device_get_sysname(device, &value); + printf(" %s==\"%s\"\n", is_parent ? "KERNELS" : "KERNEL", strempty(value)); + + value = NULL; + (void) sd_device_get_subsystem(device, &value); + printf(" %s==\"%s\"\n", is_parent ? "SUBSYSTEMS" : "SUBSYSTEM", strempty(value)); + + value = NULL; + (void) sd_device_get_driver(device, &value); + printf(" %s==\"%s\"\n", is_parent ? "DRIVERS" : "DRIVER", strempty(value)); + + FOREACH_DEVICE_SYSATTR(device, name) { + size_t len; + + if (skip_attribute(name)) + continue; + + if (sd_device_get_sysattr_value(device, name, &value) < 0) + continue; + + /* skip any values that look like a path */ + if (value[0] == '/') + continue; + + /* skip nonprintable attributes */ + len = strlen(value); + while (len > 0 && isprint((unsigned char) value[len-1])) + len--; + if (len > 0) + continue; + + if (!GREEDY_REALLOC(sysattrs, n_allocated, n_items + 1)) + return log_oom(); + + sysattrs[n_items] = (SysAttr) { + .name = name, + .value = value, + }; + n_items++; + } + + typesafe_qsort(sysattrs, n_items, sysattr_compare); + + for (size_t i = 0; i < n_items; i++) + printf(" %s{%s}==\"%s\"\n", is_parent ? "ATTRS" : "ATTR", sysattrs[i].name, sysattrs[i].value); + + puts(""); + + return 0; +} + +static int print_device_chain(sd_device *device) { + sd_device *child, *parent; + int r; + + printf("\n" + "Udevadm info starts with the device specified by the devpath and then\n" + "walks up the chain of parent devices. It prints for every device\n" + "found, all possible attributes in the udev rules key format.\n" + "A rule to match, can be composed by the attributes of the device\n" + "and the attributes from one single parent device.\n" + "\n"); + + r = print_all_attributes(device, false); + if (r < 0) + return r; + + for (child = device; sd_device_get_parent(child, &parent) >= 0; child = parent) { + r = print_all_attributes(parent, true); + if (r < 0) + return r; + } + + return 0; +} + +static int print_record(sd_device *device) { + const char *str, *val; + int i; + + (void) sd_device_get_devpath(device, &str); + printf("P: %s\n", str); + + if (sd_device_get_devname(device, &str) >= 0) { + assert_se(val = path_startswith(str, "/dev/")); + printf("N: %s\n", val); + } + + if (device_get_devlink_priority(device, &i) >= 0) + printf("L: %i\n", i); + + FOREACH_DEVICE_DEVLINK(device, str) { + assert_se(val = path_startswith(str, "/dev/")); + printf("S: %s\n", val); + } + + FOREACH_DEVICE_PROPERTY(device, str, val) + printf("E: %s=%s\n", str, val); + + puts(""); + return 0; +} + +static int stat_device(const char *name, bool export, const char *prefix) { + struct stat statbuf; + + if (stat(name, &statbuf) != 0) + return -errno; + + if (export) { + if (!prefix) + prefix = "INFO_"; + printf("%sMAJOR=%u\n" + "%sMINOR=%u\n", + prefix, major(statbuf.st_dev), + prefix, minor(statbuf.st_dev)); + } else + printf("%u:%u\n", major(statbuf.st_dev), minor(statbuf.st_dev)); + return 0; +} + +static int export_devices(void) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + sd_device *d; + int r; + + r = sd_device_enumerator_new(&e); + if (r < 0) + return log_oom(); + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return log_error_errno(r, "Failed to set allowing uninitialized flag: %m"); + + r = device_enumerator_scan_devices(e); + if (r < 0) + return log_error_errno(r, "Failed to scan devices: %m"); + + FOREACH_DEVICE_AND_SUBSYSTEM(e, d) + (void) print_record(d); + + return 0; +} + +static void cleanup_dir(DIR *dir, mode_t mask, int depth) { + struct dirent *dent; + + if (depth <= 0) + return; + + FOREACH_DIRENT_ALL(dent, dir, break) { + struct stat stats; + + if (dent->d_name[0] == '.') + continue; + if (fstatat(dirfd(dir), dent->d_name, &stats, AT_SYMLINK_NOFOLLOW) != 0) + continue; + if ((stats.st_mode & mask) != 0) + continue; + if (S_ISDIR(stats.st_mode)) { + _cleanup_closedir_ DIR *dir2 = NULL; + + dir2 = fdopendir(openat(dirfd(dir), dent->d_name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC)); + if (dir2) + cleanup_dir(dir2, mask, depth-1); + + (void) unlinkat(dirfd(dir), dent->d_name, AT_REMOVEDIR); + } else + (void) unlinkat(dirfd(dir), dent->d_name, 0); + } +} + +static void cleanup_db(void) { + _cleanup_closedir_ DIR *dir1 = NULL, *dir2 = NULL, *dir3 = NULL, *dir4 = NULL, *dir5 = NULL; + + (void) unlink("/run/udev/queue.bin"); + + dir1 = opendir("/run/udev/data"); + if (dir1) + cleanup_dir(dir1, S_ISVTX, 1); + + dir2 = opendir("/run/udev/links"); + if (dir2) + cleanup_dir(dir2, 0, 2); + + dir3 = opendir("/run/udev/tags"); + if (dir3) + cleanup_dir(dir3, 0, 2); + + dir4 = opendir("/run/udev/static_node-tags"); + if (dir4) + cleanup_dir(dir4, 0, 2); + + dir5 = opendir("/run/udev/watch"); + if (dir5) + cleanup_dir(dir5, 0, 1); +} + +static int query_device(QueryType query, sd_device* device) { + int r; + + assert(device); + + switch(query) { + case QUERY_NAME: { + const char *node; + + r = sd_device_get_devname(device, &node); + if (r < 0) + return log_error_errno(r, "No device node found: %m"); + + if (!arg_root) + assert_se(node = path_startswith(node, "/dev/")); + printf("%s\n", node); + return 0; + } + + case QUERY_SYMLINK: { + const char *devlink, *prefix = ""; + + FOREACH_DEVICE_DEVLINK(device, devlink) { + if (!arg_root) + assert_se(devlink = path_startswith(devlink, "/dev/")); + printf("%s%s", prefix, devlink); + prefix = " "; + } + puts(""); + return 0; + } + + case QUERY_PATH: { + const char *devpath; + + r = sd_device_get_devpath(device, &devpath); + if (r < 0) + return log_error_errno(r, "Failed to get device path: %m"); + + printf("%s\n", devpath); + return 0; + } + + case QUERY_PROPERTY: { + const char *key, *value; + + FOREACH_DEVICE_PROPERTY(device, key, value) + if (arg_export) + printf("%s%s='%s'\n", strempty(arg_export_prefix), key, value); + else + printf("%s=%s\n", key, value); + return 0; + } + + case QUERY_ALL: + return print_record(device); + } + + assert_not_reached("unknown query type"); + return 0; +} + +static int help(void) { + printf("%s info [OPTIONS] [DEVPATH|FILE]\n\n" + "Query sysfs or the udev database.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -q --query=TYPE Query device information:\n" + " name Name of device node\n" + " symlink Pointing to node\n" + " path sysfs device path\n" + " property The device properties\n" + " all All values\n" + " -p --path=SYSPATH sysfs device path used for query or attribute walk\n" + " -n --name=NAME Node or symlink name used for query or attribute walk\n" + " -r --root Prepend dev directory to path names\n" + " -a --attribute-walk Print all key matches walking along the chain\n" + " of parent devices\n" + " -d --device-id-of-file=FILE Print major:minor of device containing this file\n" + " -x --export Export key/value pairs\n" + " -P --export-prefix Export the key name with a prefix\n" + " -e --export-db Export the content of the udev database\n" + " -c --cleanup-db Clean up the udev database\n" + " -w --wait-for-initialization[=SECONDS]\n" + " Wait for device to be initialized\n" + , program_invocation_short_name); + + return 0; +} + +int info_main(int argc, char *argv[], void *userdata) { + _cleanup_strv_free_ char **devices = NULL; + _cleanup_free_ char *name = NULL; + int c, r; + + static const struct option options[] = { + { "name", required_argument, NULL, 'n' }, + { "path", required_argument, NULL, 'p' }, + { "query", required_argument, NULL, 'q' }, + { "attribute-walk", no_argument, NULL, 'a' }, + { "cleanup-db", no_argument, NULL, 'c' }, + { "export-db", no_argument, NULL, 'e' }, + { "root", no_argument, NULL, 'r' }, + { "device-id-of-file", required_argument, NULL, 'd' }, + { "export", no_argument, NULL, 'x' }, + { "export-prefix", required_argument, NULL, 'P' }, + { "wait-for-initialization", optional_argument, NULL, 'w' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + ActionType action = ACTION_QUERY; + QueryType query = QUERY_ALL; + + while ((c = getopt_long(argc, argv, "aced:n:p:q:rxP:w::Vh", options, NULL)) >= 0) + switch (c) { + case 'n': + case 'p': { + const char *prefix = c == 'n' ? "/dev/" : "/sys/"; + char *path; + + path = path_join(path_startswith(optarg, prefix) ? NULL : prefix, optarg); + if (!path) + return log_oom(); + + r = strv_consume(&devices, path); + if (r < 0) + return log_oom(); + break; + } + + case 'q': + action = ACTION_QUERY; + if (streq(optarg, "property") || streq(optarg, "env")) + query = QUERY_PROPERTY; + else if (streq(optarg, "name")) + query = QUERY_NAME; + else if (streq(optarg, "symlink")) + query = QUERY_SYMLINK; + else if (streq(optarg, "path")) + query = QUERY_PATH; + else if (streq(optarg, "all")) + query = QUERY_ALL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "unknown query type"); + break; + case 'r': + arg_root = true; + break; + case 'd': + action = ACTION_DEVICE_ID_FILE; + r = free_and_strdup(&name, optarg); + if (r < 0) + return log_oom(); + break; + case 'a': + action = ACTION_ATTRIBUTE_WALK; + break; + case 'e': + return export_devices(); + case 'c': + cleanup_db(); + return 0; + case 'x': + arg_export = true; + break; + case 'P': + arg_export = true; + arg_export_prefix = optarg; + break; + case 'w': + if (optarg) { + r = parse_sec(optarg, &arg_wait_for_initialization_timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value: %m"); + } else + arg_wait_for_initialization_timeout = USEC_INFINITY; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option"); + } + + if (action == ACTION_DEVICE_ID_FILE) { + if (argv[optind]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Positional arguments are not allowed with -d/--device-id-of-file."); + assert(name); + return stat_device(name, arg_export, arg_export_prefix); + } + + r = strv_extend_strv(&devices, argv + optind, false); + if (r < 0) + return log_error_errno(r, "Failed to build argument list: %m"); + + if (strv_isempty(devices)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "A device name or path is required"); + if (action == ACTION_ATTRIBUTE_WALK && strv_length(devices) > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Only one device may be specified with -a/--attribute-walk"); + + char **p; + STRV_FOREACH(p, devices) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + + r = find_device(*p, NULL, &device); + if (r == -EINVAL) + return log_error_errno(r, "Bad argument \"%s\", expected an absolute path in /dev/ or /sys or a unit name: %m", *p); + if (r < 0) + return log_error_errno(r, "Unknown device \"%s\": %m", *p); + + if (arg_wait_for_initialization_timeout > 0) { + sd_device *d; + + r = device_wait_for_initialization( + device, + NULL, + usec_add(now(CLOCK_MONOTONIC), arg_wait_for_initialization_timeout), + &d); + if (r < 0) + return r; + + sd_device_unref(device); + device = d; + } + + if (action == ACTION_QUERY) + r = query_device(query, device); + else if (action == ACTION_ATTRIBUTE_WALK) + r = print_device_chain(device); + else + assert_not_reached("Unknown action"); + if (r < 0) + return r; + } + + return 0; +} diff --git a/src/udev/udevadm-monitor.c b/src/udev/udevadm-monitor.c new file mode 100644 index 0000000..cae7f1b --- /dev/null +++ b/src/udev/udevadm-monitor.c @@ -0,0 +1,254 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "device-monitor-private.h" +#include "device-private.h" +#include "device-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "hashmap.h" +#include "set.h" +#include "signal-util.h" +#include "string-util.h" +#include "udevadm.h" +#include "virt.h" +#include "time-util.h" + +static bool arg_show_property = false; +static bool arg_print_kernel = false; +static bool arg_print_udev = false; +static Set *arg_tag_filter = NULL; +static Hashmap *arg_subsystem_filter = NULL; + +static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) { + DeviceAction action = _DEVICE_ACTION_INVALID; + const char *devpath = NULL, *subsystem = NULL; + MonitorNetlinkGroup group = PTR_TO_INT(userdata); + struct timespec ts; + + assert(device); + assert(IN_SET(group, MONITOR_GROUP_UDEV, MONITOR_GROUP_KERNEL)); + + (void) device_get_action(device, &action); + (void) sd_device_get_devpath(device, &devpath); + (void) sd_device_get_subsystem(device, &subsystem); + + assert_se(clock_gettime(CLOCK_MONOTONIC, &ts) == 0); + + printf("%-6s[%"PRI_TIME".%06"PRI_NSEC"] %-8s %s (%s)\n", + group == MONITOR_GROUP_UDEV ? "UDEV" : "KERNEL", + ts.tv_sec, (nsec_t)ts.tv_nsec/1000, + strna(device_action_to_string(action)), + devpath, subsystem); + + if (arg_show_property) { + const char *key, *value; + + FOREACH_DEVICE_PROPERTY(device, key, value) + printf("%s=%s\n", key, value); + + printf("\n"); + } + + return 0; +} + +static int setup_monitor(MonitorNetlinkGroup sender, sd_event *event, sd_device_monitor **ret) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; + const char *subsystem, *devtype, *tag; + int r; + + r = device_monitor_new_full(&monitor, sender, -1); + if (r < 0) + return log_error_errno(r, "Failed to create netlink socket: %m"); + + (void) sd_device_monitor_set_receive_buffer_size(monitor, 128*1024*1024); + + r = sd_device_monitor_attach_event(monitor, event); + if (r < 0) + return log_error_errno(r, "Failed to attach event: %m"); + + HASHMAP_FOREACH_KEY(devtype, subsystem, arg_subsystem_filter) { + r = sd_device_monitor_filter_add_match_subsystem_devtype(monitor, subsystem, devtype); + if (r < 0) + return log_error_errno(r, "Failed to apply subsystem filter '%s%s%s': %m", + subsystem, devtype ? "/" : "", strempty(devtype)); + } + + SET_FOREACH(tag, arg_tag_filter) { + r = sd_device_monitor_filter_add_match_tag(monitor, tag); + if (r < 0) + return log_error_errno(r, "Failed to apply tag filter '%s': %m", tag); + } + + r = sd_device_monitor_start(monitor, device_monitor_handler, INT_TO_PTR(sender)); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + (void) sd_event_source_set_description(sd_device_monitor_get_event_source(monitor), + sender == MONITOR_GROUP_UDEV ? "device-monitor-udev" : "device-monitor-kernel"); + + *ret = TAKE_PTR(monitor); + return 0; +} + +static int help(void) { + printf("%s monitor [OPTIONS]\n\n" + "Listen to kernel and udev events.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -p --property Print the event properties\n" + " -k --kernel Print kernel uevents\n" + " -u --udev Print udev events\n" + " -s --subsystem-match=SUBSYSTEM[/DEVTYPE] Filter events by subsystem\n" + " -t --tag-match=TAG Filter events by tag\n" + , program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "property", no_argument, NULL, 'p' }, + { "environment", no_argument, NULL, 'e' }, /* alias for -p */ + { "kernel", no_argument, NULL, 'k' }, + { "udev", no_argument, NULL, 'u' }, + { "subsystem-match", required_argument, NULL, 's' }, + { "tag-match", required_argument, NULL, 't' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int r, c; + + while ((c = getopt_long(argc, argv, "pekus:t:Vh", options, NULL)) >= 0) + switch (c) { + case 'p': + case 'e': + arg_show_property = true; + break; + case 'k': + arg_print_kernel = true; + break; + case 'u': + arg_print_udev = true; + break; + case 's': { + _cleanup_free_ char *subsystem = NULL, *devtype = NULL; + const char *slash; + + slash = strchr(optarg, '/'); + if (slash) { + devtype = strdup(slash + 1); + if (!devtype) + return -ENOMEM; + + subsystem = strndup(optarg, slash - optarg); + } else + subsystem = strdup(optarg); + + if (!subsystem) + return -ENOMEM; + + r = hashmap_ensure_allocated(&arg_subsystem_filter, NULL); + if (r < 0) + return r; + + r = hashmap_put(arg_subsystem_filter, subsystem, devtype); + if (r < 0) + return r; + + subsystem = devtype = NULL; + break; + } + case 't': + /* optarg is stored in argv[], so we don't need to copy it */ + r = set_ensure_put(&arg_tag_filter, &string_hash_ops, optarg); + if (r < 0) + return r; + break; + + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option."); + } + + if (!arg_print_kernel && !arg_print_udev) { + arg_print_kernel = true; + arg_print_udev = true; + } + + return 1; +} + +int monitor_main(int argc, char *argv[], void *userdata) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *kernel_monitor = NULL, *udev_monitor = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + goto finalize; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + /* Callers are expecting to see events as they happen: Line buffering */ + setlinebuf(stdout); + + r = sd_event_default(&event); + if (r < 0) { + log_error_errno(r, "Failed to initialize event: %m"); + goto finalize; + } + + assert_se(sigprocmask_many(SIG_UNBLOCK, NULL, SIGTERM, SIGINT, -1) >= 0); + (void) sd_event_add_signal(event, NULL, SIGTERM, NULL, NULL); + (void) sd_event_add_signal(event, NULL, SIGINT, NULL, NULL); + + printf("monitor will print the received events for:\n"); + if (arg_print_udev) { + r = setup_monitor(MONITOR_GROUP_UDEV, event, &udev_monitor); + if (r < 0) + goto finalize; + + printf("UDEV - the event which udev sends out after rule processing\n"); + } + + if (arg_print_kernel) { + r = setup_monitor(MONITOR_GROUP_KERNEL, event, &kernel_monitor); + if (r < 0) + goto finalize; + + printf("KERNEL - the kernel uevent\n"); + } + printf("\n"); + + r = sd_event_loop(event); + if (r < 0) { + log_error_errno(r, "Failed to run event loop: %m"); + goto finalize; + } + + r = 0; + +finalize: + hashmap_free_free_free(arg_subsystem_filter); + set_free(arg_tag_filter); + + return r; +} diff --git a/src/udev/udevadm-settle.c b/src/udev/udevadm-settle.c new file mode 100644 index 0000000..2bd5853 --- /dev/null +++ b/src/udev/udevadm-settle.c @@ -0,0 +1,226 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2009 Canonical Ltd. + * Copyright © 2009 Scott James Remnant <scott@netsplit.com> + */ + +#include <errno.h> +#include <getopt.h> +#include <poll.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "sd-bus.h" +#include "sd-login.h" +#include "sd-messages.h" + +#include "bus-util.h" +#include "io-util.h" +#include "libudev-util.h" +#include "string-util.h" +#include "strv.h" +#include "time-util.h" +#include "udev-ctrl.h" +#include "udevadm.h" +#include "unit-def.h" +#include "util.h" +#include "virt.h" + +static usec_t arg_timeout = 120 * USEC_PER_SEC; +static const char *arg_exists = NULL; + +static int help(void) { + printf("%s settle [OPTIONS]\n\n" + "Wait for pending udev events.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -t --timeout=SEC Maximum time to wait for events\n" + " -E --exit-if-exists=FILE Stop waiting if file exists\n" + , program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "timeout", required_argument, NULL, 't' }, + { "exit-if-exists", required_argument, NULL, 'E' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + { "seq-start", required_argument, NULL, 's' }, /* removed */ + { "seq-end", required_argument, NULL, 'e' }, /* removed */ + { "quiet", no_argument, NULL, 'q' }, /* removed */ + {} + }; + + int c, r; + + while ((c = getopt_long(argc, argv, "t:E:Vhs:e:q", options, NULL)) >= 0) { + switch (c) { + case 't': + r = parse_sec(optarg, &arg_timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg); + break; + case 'E': + arg_exists = optarg; + break; + case 'V': + return print_version(); + case 'h': + return help(); + case 's': + case 'e': + case 'q': + return log_info_errno(SYNTHETIC_ERRNO(EINVAL), + "Option -%c no longer supported.", + c); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option."); + } + } + + return 1; +} + +static int emit_deprecation_warning(void) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_strv_free_ char **a = NULL; + _cleanup_free_ char *unit = NULL; + int r; + + r = sd_pid_get_unit(0, &unit); + if (r < 0) { + log_debug_errno(r, "Failed to determine unit we run in, ignoring: %m"); + return 0; + } + + if (!streq(unit, "systemd-udev-settle.service")) + return 0; + + r = bus_connect_system_systemd(&bus); + if (r < 0) + log_debug_errno(r, "Failed to open connection to systemd, skipping dependency queries: %m"); + else { + _cleanup_strv_free_ char **b = NULL; + _cleanup_free_ char *unit_path = NULL; + + unit_path = unit_dbus_path_from_name("systemd-udev-settle.service"); + if (!unit_path) + return -ENOMEM; + + (void) sd_bus_get_property_strv( + bus, + "org.freedesktop.systemd1", + unit_path, + "org.freedesktop.systemd1.Unit", + "WantedBy", + NULL, + &a); + + (void) sd_bus_get_property_strv( + bus, + "org.freedesktop.systemd1", + unit_path, + "org.freedesktop.systemd1.Unit", + "RequiredBy", + NULL, + &b); + + r = strv_extend_strv(&a, b, true); + if (r < 0) + return r; + } + + if (strv_isempty(a)) + /* Print a simple message if we cannot determine the dependencies */ + log_notice("systemd-udev-settle.service is deprecated."); + else { + /* Print a longer, structured message if we can acquire the dependencies (this should be the + * common case). This is hooked up with a catalog entry and everything. */ + _cleanup_free_ char *t = NULL; + + t = strv_join(a, ", "); + if (!t) + return -ENOMEM; + + log_struct(LOG_NOTICE, + "MESSAGE=systemd-udev-settle.service is deprecated. Please fix %s not to pull it in.", t, + "OFFENDING_UNITS=%s", t, + "MESSAGE_ID=" SD_MESSAGE_SYSTEMD_UDEV_SETTLE_DEPRECATED_STR); + } + + return 0; +} + +int settle_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_queue_unrefp) struct udev_queue *queue = NULL; + usec_t deadline; + int r, fd; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + deadline = now(CLOCK_MONOTONIC) + arg_timeout; + + /* guarantee that the udev daemon isn't pre-processing */ + if (getuid() == 0) { + _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *uctrl = NULL; + + if (udev_ctrl_new(&uctrl) >= 0) { + r = udev_ctrl_send_ping(uctrl); + if (r < 0) { + log_debug_errno(r, "Failed to connect to udev daemon: %m"); + return 0; + } + + r = udev_ctrl_wait(uctrl, MAX(5 * USEC_PER_SEC, arg_timeout)); + if (r < 0) + return log_error_errno(r, "Failed to wait for daemon to reply: %m"); + } + } + + queue = udev_queue_new(NULL); + if (!queue) + return log_error_errno(errno, "Failed to get udev queue: %m"); + + fd = udev_queue_get_fd(queue); + if (fd < 0) { + log_debug_errno(fd, "Queue is empty, nothing to watch: %m"); + return 0; + } + + (void) emit_deprecation_warning(); + + for (;;) { + if (arg_exists && access(arg_exists, F_OK) >= 0) + return 0; + + /* exit if queue is empty */ + if (udev_queue_get_queue_is_empty(queue)) + return 0; + + if (now(CLOCK_MONOTONIC) >= deadline) + return -ETIMEDOUT; + + /* wake up when queue becomes empty */ + r = fd_wait_for_event(fd, POLLIN, MSEC_PER_SEC); + if (r < 0) + return r; + if (r & POLLIN) { + r = udev_queue_flush(queue); + if (r < 0) + return log_error_errno(r, "Failed to flush queue: %m"); + } + } +} diff --git a/src/udev/udevadm-test-builtin.c b/src/udev/udevadm-test-builtin.c new file mode 100644 index 0000000..8995e5c --- /dev/null +++ b/src/udev/udevadm-test-builtin.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +#include "log.h" +#include "udev-builtin.h" +#include "udevadm.h" +#include "udevadm-util.h" + +static const char *arg_command = NULL; +static const char *arg_syspath = NULL; + +static int help(void) { + printf("%s test-builtin [OPTIONS] COMMAND DEVPATH\n\n" + "Test a built-in command.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n\n" + "Commands:\n" + , program_invocation_short_name); + + udev_builtin_list(); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int c; + + while ((c = getopt_long(argc, argv, "Vh", options, NULL)) >= 0) + switch (c) { + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option"); + } + + arg_command = argv[optind++]; + if (!arg_command) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Command missing."); + + arg_syspath = argv[optind++]; + if (!arg_syspath) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "syspath missing."); + + return 1; +} + +int builtin_main(int argc, char *argv[], void *userdata) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + UdevBuiltinCommand cmd; + int r; + + log_set_max_level(LOG_DEBUG); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + udev_builtin_init(); + + cmd = udev_builtin_lookup(arg_command); + if (cmd < 0) { + log_error("Unknown command '%s'", arg_command); + r = -EINVAL; + goto finish; + } + + r = find_device(arg_syspath, "/sys", &dev); + if (r < 0) { + log_error_errno(r, "Failed to open device '%s': %m", arg_syspath); + goto finish; + } + + r = udev_builtin_run(dev, cmd, arg_command, true); + if (r < 0) + log_debug_errno(r, "Builtin command '%s' fails: %m", arg_command); + +finish: + udev_builtin_exit(); + return r; +} diff --git a/src/udev/udevadm-test.c b/src/udev/udevadm-test.c new file mode 100644 index 0000000..a029622 --- /dev/null +++ b/src/udev/udevadm-test.c @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2003-2004 Greg Kroah-Hartman <greg@kroah.com> + */ + +#include <errno.h> +#include <getopt.h> +#include <signal.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/signalfd.h> +#include <unistd.h> + +#include "sd-device.h" + +#include "device-private.h" +#include "device-util.h" +#include "libudev-util.h" +#include "path-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" +#include "udev-event.h" +#include "udevadm.h" + +static const char *arg_action = "add"; +static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY; +static char arg_syspath[UTIL_PATH_SIZE] = {}; + +static int help(void) { + + printf("%s test [OPTIONS] DEVPATH\n\n" + "Test an event run.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -a --action=ACTION|help Set action string\n" + " -N --resolve-names=early|late|never When to resolve names\n" + , program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "action", required_argument, NULL, 'a' }, + { "resolve-names", required_argument, NULL, 'N' }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + + int c; + + while ((c = getopt_long(argc, argv, "a:N:Vh", options, NULL)) >= 0) + switch (c) { + case 'a': { + DeviceAction a; + + if (streq(optarg, "help")) { + dump_device_action_table(); + return 0; + } + + a = device_action_from_string(optarg); + if (a < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid action '%s'", optarg); + + arg_action = optarg; + break; + } + case 'N': + arg_resolve_name_timing = resolve_name_timing_from_string(optarg); + if (arg_resolve_name_timing < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--resolve-names= must be early, late or never"); + break; + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option"); + } + + if (!argv[optind]) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "syspath parameter missing."); + + /* add /sys if needed */ + if (!path_startswith(argv[optind], "/sys")) + strscpyl(arg_syspath, sizeof(arg_syspath), "/sys", argv[optind], NULL); + else + strscpy(arg_syspath, sizeof(arg_syspath), argv[optind]); + + return 1; +} + +int test_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_(udev_event_freep) UdevEvent *event = NULL; + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + const char *cmd, *key, *value; + sigset_t mask, sigmask_orig; + void *val; + int r; + + log_set_max_level(LOG_DEBUG); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + printf("This program is for debugging only, it does not run any program\n" + "specified by a RUN key. It may show incorrect results, because\n" + "some values may be different, or not available at a simulation run.\n" + "\n"); + + assert_se(sigprocmask(SIG_SETMASK, NULL, &sigmask_orig) >= 0); + + udev_builtin_init(); + + r = udev_rules_load(&rules, arg_resolve_name_timing); + if (r < 0) { + log_error_errno(r, "Failed to read udev rules: %m"); + goto out; + } + + r = device_new_from_synthetic_event(&dev, arg_syspath, arg_action); + if (r < 0) { + log_error_errno(r, "Failed to open device '%s': %m", arg_syspath); + goto out; + } + + /* don't read info from the db */ + device_seal(dev); + + event = udev_event_new(dev, 0, NULL); + + assert_se(sigfillset(&mask) >= 0); + assert_se(sigprocmask(SIG_SETMASK, &mask, &sigmask_orig) >= 0); + + udev_event_execute_rules(event, 60 * USEC_PER_SEC, SIGKILL, NULL, rules); + + FOREACH_DEVICE_PROPERTY(dev, key, value) + printf("%s=%s\n", key, value); + + ORDERED_HASHMAP_FOREACH_KEY(val, cmd, event->run_list) { + char program[UTIL_PATH_SIZE]; + + (void) udev_event_apply_format(event, cmd, program, sizeof(program), false); + printf("run: '%s'\n", program); + } + + r = 0; +out: + udev_builtin_exit(); + return r; +} diff --git a/src/udev/udevadm-trigger.c b/src/udev/udevadm-trigger.c new file mode 100644 index 0000000..5c74184 --- /dev/null +++ b/src/udev/udevadm-trigger.c @@ -0,0 +1,393 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "device-enumerator-private.h" +#include "device-private.h" +#include "fd-util.h" +#include "fileio.h" +#include "path-util.h" +#include "process-util.h" +#include "set.h" +#include "string-util.h" +#include "strv.h" +#include "udevadm.h" +#include "udevadm-util.h" +#include "udev-ctrl.h" +#include "virt.h" + +static bool arg_verbose = false; +static bool arg_dry_run = false; + +static int exec_list(sd_device_enumerator *e, const char *action, Set **settle_set) { + sd_device *d; + int r, ret = 0; + + FOREACH_DEVICE_AND_SUBSYSTEM(e, d) { + _cleanup_free_ char *filename = NULL; + const char *syspath; + + if (sd_device_get_syspath(d, &syspath) < 0) + continue; + + if (arg_verbose) + printf("%s\n", syspath); + if (arg_dry_run) + continue; + + filename = path_join(syspath, "uevent"); + if (!filename) + return log_oom(); + + r = write_string_file(filename, action, WRITE_STRING_FILE_DISABLE_BUFFER); + if (r < 0) { + bool ignore = IN_SET(r, -ENOENT, -ENODEV); + + log_full_errno(ignore ? LOG_DEBUG : LOG_ERR, r, + "Failed to write '%s' to '%s'%s: %m", + action, filename, ignore ? ", ignoring" : ""); + if (IN_SET(r, -EACCES, -EROFS)) + /* Inovoked by unpriviledged user, or read only filesystem. Return earlier. */ + return r; + if (ret == 0 && !ignore) + ret = r; + continue; + } + + if (settle_set) { + r = set_put_strdup(settle_set, syspath); + if (r < 0) + return log_oom(); + } + } + + return ret; +} + +static int device_monitor_handler(sd_device_monitor *m, sd_device *dev, void *userdata) { + _cleanup_free_ char *val = NULL; + Set *settle_set = userdata; + const char *syspath; + + assert(dev); + assert(settle_set); + + if (sd_device_get_syspath(dev, &syspath) < 0) + return 0; + + if (arg_verbose) + printf("settle %s\n", syspath); + + val = set_remove(settle_set, syspath); + if (!val) + log_debug("Got epoll event on syspath %s not present in syspath set", syspath); + + if (set_isempty(settle_set)) + return sd_event_exit(sd_device_monitor_get_event(m), 0); + + return 0; +} + +static char* keyval(const char *str, const char **key, const char **val) { + char *buf, *pos; + + buf = strdup(str); + if (!buf) + return NULL; + + pos = strchr(buf, '='); + if (pos) { + pos[0] = 0; + pos++; + } + + *key = buf; + *val = pos; + + return buf; +} + +static int help(void) { + printf("%s trigger [OPTIONS] DEVPATH\n\n" + "Request events from the kernel.\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -v --verbose Print the list of devices while running\n" + " -n --dry-run Do not actually trigger the events\n" + " -t --type= Type of events to trigger\n" + " devices sysfs devices (default)\n" + " subsystems sysfs subsystems and drivers\n" + " -c --action=ACTION|help Event action value, default is \"change\"\n" + " -s --subsystem-match=SUBSYSTEM Trigger devices from a matching subsystem\n" + " -S --subsystem-nomatch=SUBSYSTEM Exclude devices from a matching subsystem\n" + " -a --attr-match=FILE[=VALUE] Trigger devices with a matching attribute\n" + " -A --attr-nomatch=FILE[=VALUE] Exclude devices with a matching attribute\n" + " -p --property-match=KEY=VALUE Trigger devices with a matching property\n" + " -g --tag-match=KEY=VALUE Trigger devices with a matching property\n" + " -y --sysname-match=NAME Trigger devices with this /sys path\n" + " --name-match=NAME Trigger devices with this /dev name\n" + " -b --parent-match=NAME Trigger devices with that parent device\n" + " -w --settle Wait for the triggered events to complete\n" + " --wait-daemon[=SECONDS] Wait for udevd daemon to be initialized\n" + " before triggering uevents\n" + , program_invocation_short_name); + + return 0; +} + +int trigger_main(int argc, char *argv[], void *userdata) { + enum { + ARG_NAME = 0x100, + ARG_PING, + }; + + static const struct option options[] = { + { "verbose", no_argument, NULL, 'v' }, + { "dry-run", no_argument, NULL, 'n' }, + { "type", required_argument, NULL, 't' }, + { "action", required_argument, NULL, 'c' }, + { "subsystem-match", required_argument, NULL, 's' }, + { "subsystem-nomatch", required_argument, NULL, 'S' }, + { "attr-match", required_argument, NULL, 'a' }, + { "attr-nomatch", required_argument, NULL, 'A' }, + { "property-match", required_argument, NULL, 'p' }, + { "tag-match", required_argument, NULL, 'g' }, + { "sysname-match", required_argument, NULL, 'y' }, + { "name-match", required_argument, NULL, ARG_NAME }, + { "parent-match", required_argument, NULL, 'b' }, + { "settle", no_argument, NULL, 'w' }, + { "wait-daemon", optional_argument, NULL, ARG_PING }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, + {} + }; + enum { + TYPE_DEVICES, + TYPE_SUBSYSTEMS, + } device_type = TYPE_DEVICES; + const char *action = "change"; + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *m = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_set_free_ Set *settle_set = NULL; + usec_t ping_timeout_usec = 5 * USEC_PER_SEC; + bool settle = false, ping = false; + int c, r; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return r; + + while ((c = getopt_long(argc, argv, "vnt:c:s:S:a:A:p:g:y:b:wVh", options, NULL)) >= 0) { + _cleanup_free_ char *buf = NULL; + const char *key, *val; + + switch (c) { + case 'v': + arg_verbose = true; + break; + case 'n': + arg_dry_run = true; + break; + case 't': + if (streq(optarg, "devices")) + device_type = TYPE_DEVICES; + else if (streq(optarg, "subsystems")) + device_type = TYPE_SUBSYSTEMS; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type --type=%s", optarg); + break; + case 'c': + if (streq(optarg, "help")) { + dump_device_action_table(); + return 0; + } + if (device_action_from_string(optarg) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown action '%s'", optarg); + + action = optarg; + break; + case 's': + r = sd_device_enumerator_add_match_subsystem(e, optarg, true); + if (r < 0) + return log_error_errno(r, "Failed to add subsystem match '%s': %m", optarg); + break; + case 'S': + r = sd_device_enumerator_add_match_subsystem(e, optarg, false); + if (r < 0) + return log_error_errno(r, "Failed to add negative subsystem match '%s': %m", optarg); + break; + case 'a': + buf = keyval(optarg, &key, &val); + if (!buf) + return log_oom(); + r = sd_device_enumerator_add_match_sysattr(e, key, val, true); + if (r < 0) + return log_error_errno(r, "Failed to add sysattr match '%s=%s': %m", key, val); + break; + case 'A': + buf = keyval(optarg, &key, &val); + if (!buf) + return log_oom(); + r = sd_device_enumerator_add_match_sysattr(e, key, val, false); + if (r < 0) + return log_error_errno(r, "Failed to add negative sysattr match '%s=%s': %m", key, val); + break; + case 'p': + buf = keyval(optarg, &key, &val); + if (!buf) + return log_oom(); + r = sd_device_enumerator_add_match_property(e, key, val); + if (r < 0) + return log_error_errno(r, "Failed to add property match '%s=%s': %m", key, val); + break; + case 'g': + r = sd_device_enumerator_add_match_tag(e, optarg); + if (r < 0) + return log_error_errno(r, "Failed to add tag match '%s': %m", optarg); + break; + case 'y': + r = sd_device_enumerator_add_match_sysname(e, optarg); + if (r < 0) + return log_error_errno(r, "Failed to add sysname match '%s': %m", optarg); + break; + case 'b': { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(optarg, "/sys", &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", optarg); + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", optarg); + break; + } + case 'w': + settle = true; + break; + + case ARG_NAME: { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(optarg, "/dev/", &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", optarg); + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", optarg); + break; + } + + case ARG_PING: { + ping = true; + if (optarg) { + r = parse_sec(optarg, &ping_timeout_usec); + if (r < 0) + log_error_errno(r, "Failed to parse timeout value '%s', ignoring: %m", optarg); + } + break; + } + + case 'V': + return print_version(); + case 'h': + return help(); + case '?': + return -EINVAL; + default: + assert_not_reached("Unknown option"); + } + } + + if (ping) { + _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *uctrl = NULL; + + r = udev_ctrl_new(&uctrl); + if (r < 0) + return log_error_errno(r, "Failed to initialize udev control: %m"); + + r = udev_ctrl_send_ping(uctrl); + if (r < 0) + return log_error_errno(r, "Failed to connect to udev daemon: %m"); + + r = udev_ctrl_wait(uctrl, ping_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to wait for daemon to reply: %m"); + } + + for (; optind < argc; optind++) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + + r = find_device(argv[optind], NULL, &dev); + if (r < 0) + return log_error_errno(r, "Failed to open the device '%s': %m", argv[optind]); + + r = device_enumerator_add_match_parent_incremental(e, dev); + if (r < 0) + return log_error_errno(r, "Failed to add parent match '%s': %m", argv[optind]); + } + + if (settle) { + settle_set = set_new(&string_hash_ops_free); + if (!settle_set) + return log_oom(); + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get default event: %m"); + + r = sd_device_monitor_new(&m); + if (r < 0) + return log_error_errno(r, "Failed to create device monitor object: %m"); + + r = sd_device_monitor_attach_event(m, event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to device monitor: %m"); + + r = sd_device_monitor_start(m, device_monitor_handler, settle_set); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + } + + switch (device_type) { + case TYPE_SUBSYSTEMS: + r = device_enumerator_scan_subsystems(e); + if (r < 0) + return log_error_errno(r, "Failed to scan subsystems: %m"); + break; + case TYPE_DEVICES: + r = device_enumerator_scan_devices(e); + if (r < 0) + return log_error_errno(r, "Failed to scan devices: %m"); + break; + default: + assert_not_reached("Unknown device type"); + } + + r = exec_list(e, action, settle ? &settle_set : NULL); + if (r < 0) + return r; + + if (event && !set_isempty(settle_set)) { + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + } + + return 0; +} diff --git a/src/udev/udevadm-util.c b/src/udev/udevadm-util.c new file mode 100644 index 0000000..39d0c7e --- /dev/null +++ b/src/udev/udevadm-util.c @@ -0,0 +1,95 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> + +#include "alloc-util.h" +#include "bus-error.h" +#include "bus-util.h" +#include "device-private.h" +#include "path-util.h" +#include "udevadm-util.h" +#include "unit-name.h" + +static int find_device_from_path(const char *path, sd_device **ret) { + if (path_startswith(path, "/sys/")) + return sd_device_new_from_syspath(ret, path); + + if (path_startswith(path, "/dev/")) { + struct stat st; + + if (stat(path, &st) < 0) + return -errno; + + return device_new_from_stat_rdev(ret, &st); + } + + return -EINVAL; +} + +static int find_device_from_unit(const char *unit_name, sd_device **ret) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *unit_path = NULL, *syspath = NULL; + int r; + + if (!unit_name_is_valid(unit_name, UNIT_NAME_PLAIN)) + return -EINVAL; + + if (unit_name_to_type(unit_name) != UNIT_DEVICE) + return -EINVAL; + + r = bus_connect_system_systemd(&bus); + if (r < 0) { + _cleanup_free_ char *path = NULL; + + log_debug_errno(r, "Failed to open connection to systemd, using unit name as syspath: %m"); + + r = unit_name_to_path(unit_name, &path); + if (r < 0) + return log_debug_errno(r, "Failed to convert \"%s\" to a device path: %m", unit_name); + + return find_device_from_path(path, ret); + } + + unit_path = unit_dbus_path_from_name(unit_name); + if (!unit_path) + return -ENOMEM; + + r = sd_bus_get_property_string( + bus, + "org.freedesktop.systemd1", + unit_path, + "org.freedesktop.systemd1.Device", + "SysFSPath", + &error, + &syspath); + if (r < 0) + return log_debug_errno(r, "Failed to get SysFSPath= dbus property for %s: %s", + unit_name, bus_error_message(&error, r)); + + return sd_device_new_from_syspath(ret, syspath); +} + +int find_device(const char *id, const char *prefix, sd_device **ret) { + _cleanup_free_ char *path = NULL; + int r; + + assert(id); + assert(ret); + + if (prefix) { + if (!path_startswith(id, prefix)) { + id = path = path_join(prefix, id); + if (!path) + return -ENOMEM; + } + } else { + /* In cases where the argument is generic (no prefix specified), + * check if the argument looks like a device unit name. */ + r = find_device_from_unit(id, ret); + if (r >= 0) + return r; + } + + return find_device_from_path(id, ret); +} diff --git a/src/udev/udevadm-util.h b/src/udev/udevadm-util.h new file mode 100644 index 0000000..91587c5 --- /dev/null +++ b/src/udev/udevadm-util.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include "sd-device.h" + +int find_device(const char *id, const char *prefix, sd_device **ret); diff --git a/src/udev/udevadm.c b/src/udev/udevadm.c new file mode 100644 index 0000000..408e4a3 --- /dev/null +++ b/src/udev/udevadm.c @@ -0,0 +1,134 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> + +#include "alloc-util.h" +#include "main-func.h" +#include "pretty-print.h" +#include "selinux-util.h" +#include "string-util.h" +#include "udevadm.h" +#include "udevd.h" +#include "udev-util.h" +#include "verbs.h" +#include "util.h" + +static int help(void) { + static const char *const short_descriptions[][2] = { + { "info", "Query sysfs or the udev database" }, + { "trigger", "Request events from the kernel" }, + { "settle", "Wait for pending udev events" }, + { "control", "Control the udev daemon" }, + { "monitor", "Listen to kernel and udev events" }, + { "test", "Test an event run" }, + { "test-builtin", "Test a built-in command" }, + }; + + _cleanup_free_ char *link = NULL; + size_t i; + int r; + + r = terminal_urlify_man("udevadm", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s [--help] [--version] [--debug] COMMAND [COMMAND OPTIONS]\n\n" + "Send control commands or test the device manager.\n\n" + "Commands:\n" + , program_invocation_short_name); + + for (i = 0; i < ELEMENTSOF(short_descriptions); i++) + printf(" %-12s %s\n", short_descriptions[i][0], short_descriptions[i][1]); + + printf("\nSee the %s for details.\n", link); + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + {} + }; + int c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "+dhV", options, NULL)) >= 0) + switch (c) { + + case 'd': + log_set_max_level(LOG_DEBUG); + break; + + case 'h': + return help(); + + case 'V': + return print_version(); + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + + return 1; /* work to do */ +} + +static int version_main(int argc, char *argv[], void *userdata) { + return print_version(); +} + +static int help_main(int argc, char *argv[], void *userdata) { + return help(); +} + +static int udevadm_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "info", VERB_ANY, VERB_ANY, 0, info_main }, + { "trigger", VERB_ANY, VERB_ANY, 0, trigger_main }, + { "settle", VERB_ANY, VERB_ANY, 0, settle_main }, + { "control", VERB_ANY, VERB_ANY, 0, control_main }, + { "monitor", VERB_ANY, VERB_ANY, 0, monitor_main }, + { "hwdb", VERB_ANY, VERB_ANY, 0, hwdb_main }, + { "test", VERB_ANY, VERB_ANY, 0, test_main }, + { "test-builtin", VERB_ANY, VERB_ANY, 0, builtin_main }, + { "version", VERB_ANY, VERB_ANY, 0, version_main }, + { "help", VERB_ANY, VERB_ANY, 0, help_main }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + if (strstr(program_invocation_short_name, "udevd")) + return run_udevd(argc, argv); + + udev_parse_config(); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + log_set_max_level_realm(LOG_REALM_SYSTEMD, log_get_max_level()); + + r = mac_selinux_init(); + if (r < 0) + return r; + + return udevadm_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/udevadm.h b/src/udev/udevadm.h new file mode 100644 index 0000000..162bbb9 --- /dev/null +++ b/src/udev/udevadm.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include <stdio.h> + +#include "build.h" +#include "macro.h" + +int info_main(int argc, char *argv[], void *userdata); +int trigger_main(int argc, char *argv[], void *userdata); +int settle_main(int argc, char *argv[], void *userdata); +int control_main(int argc, char *argv[], void *userdata); +int monitor_main(int argc, char *argv[], void *userdata); +int hwdb_main(int argc, char *argv[], void *userdata); +int test_main(int argc, char *argv[], void *userdata); +int builtin_main(int argc, char *argv[], void *userdata); + +static inline int print_version(void) { + /* Dracut relies on the version being a single integer */ + puts(STRINGIFY(PROJECT_VERSION)); + return 0; +} diff --git a/src/udev/udevd.c b/src/udev/udevd.c new file mode 100644 index 0000000..d24b8d4 --- /dev/null +++ b/src/udev/udevd.c @@ -0,0 +1,1960 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright © 2004 Chris Friesen <chris_friesen@sympatico.ca> + * Copyright © 2009 Canonical Ltd. + * Copyright © 2009 Scott James Remnant <scott@netsplit.com> + */ + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/epoll.h> +#include <sys/file.h> +#include <sys/inotify.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/signalfd.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "sd-daemon.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "build.h" +#include "cgroup-util.h" +#include "cpu-set-util.h" +#include "dev-setup.h" +#include "device-monitor-private.h" +#include "device-private.h" +#include "device-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "hashmap.h" +#include "io-util.h" +#include "libudev-device-internal.h" +#include "limits-util.h" +#include "list.h" +#include "main-func.h" +#include "mkdir.h" +#include "netlink-util.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "proc-cmdline.h" +#include "process-util.h" +#include "selinux-util.h" +#include "signal-util.h" +#include "socket-util.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "syslog-util.h" +#include "udevd.h" +#include "udev-builtin.h" +#include "udev-ctrl.h" +#include "udev-event.h" +#include "udev-util.h" +#include "udev-watch.h" +#include "user-util.h" + +#define WORKER_NUM_MAX 2048U + +static bool arg_debug = false; +static int arg_daemonize = false; +static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY; +static unsigned arg_children_max = 0; +static usec_t arg_exec_delay_usec = 0; +static usec_t arg_event_timeout_usec = 180 * USEC_PER_SEC; +static int arg_timeout_signal = SIGKILL; +static bool arg_blockdev_read_only = false; + +typedef struct Manager { + sd_event *event; + Hashmap *workers; + LIST_HEAD(struct event, events); + const char *cgroup; + pid_t pid; /* the process that originally allocated the manager object */ + + UdevRules *rules; + Hashmap *properties; + + sd_netlink *rtnl; + + sd_device_monitor *monitor; + struct udev_ctrl *ctrl; + int fd_inotify; + int worker_watch[2]; + + sd_event_source *inotify_event; + sd_event_source *kill_workers_event; + + usec_t last_usec; + + bool stop_exec_queue:1; + bool exit:1; +} Manager; + +enum event_state { + EVENT_UNDEF, + EVENT_QUEUED, + EVENT_RUNNING, +}; + +struct event { + Manager *manager; + struct worker *worker; + enum event_state state; + + sd_device *dev; + sd_device *dev_kernel; /* clone of originally received device */ + + uint64_t seqnum; + uint64_t delaying_seqnum; + + sd_event_source *timeout_warning_event; + sd_event_source *timeout_event; + + LIST_FIELDS(struct event, event); +}; + +static void event_queue_cleanup(Manager *manager, enum event_state type); + +enum worker_state { + WORKER_UNDEF, + WORKER_RUNNING, + WORKER_IDLE, + WORKER_KILLED, +}; + +struct worker { + Manager *manager; + pid_t pid; + sd_device_monitor *monitor; + enum worker_state state; + struct event *event; +}; + +/* passed from worker to main process */ +struct worker_message { +}; + +static void event_free(struct event *event) { + if (!event) + return; + + assert(event->manager); + + LIST_REMOVE(event, event->manager->events, event); + sd_device_unref(event->dev); + sd_device_unref(event->dev_kernel); + + sd_event_source_unref(event->timeout_warning_event); + sd_event_source_unref(event->timeout_event); + + if (event->worker) + event->worker->event = NULL; + + /* only clean up the queue from the process that created it */ + if (LIST_IS_EMPTY(event->manager->events) && + event->manager->pid == getpid_cached()) + if (unlink("/run/udev/queue") < 0) + log_warning_errno(errno, "Failed to unlink /run/udev/queue: %m"); + + free(event); +} + +static void worker_free(struct worker *worker) { + if (!worker) + return; + + assert(worker->manager); + + hashmap_remove(worker->manager->workers, PID_TO_PTR(worker->pid)); + sd_device_monitor_unref(worker->monitor); + event_free(worker->event); + + free(worker); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct worker *, worker_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(worker_hash_op, void, trivial_hash_func, trivial_compare_func, struct worker, worker_free); + +static int worker_new(struct worker **ret, Manager *manager, sd_device_monitor *worker_monitor, pid_t pid) { + _cleanup_(worker_freep) struct worker *worker = NULL; + int r; + + assert(ret); + assert(manager); + assert(worker_monitor); + assert(pid > 1); + + /* close monitor, but keep address around */ + device_monitor_disconnect(worker_monitor); + + worker = new(struct worker, 1); + if (!worker) + return -ENOMEM; + + *worker = (struct worker) { + .manager = manager, + .monitor = sd_device_monitor_ref(worker_monitor), + .pid = pid, + }; + + r = hashmap_ensure_allocated(&manager->workers, &worker_hash_op); + if (r < 0) + return r; + + r = hashmap_put(manager->workers, PID_TO_PTR(pid), worker); + if (r < 0) + return r; + + *ret = TAKE_PTR(worker); + + return 0; +} + +static int on_event_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + struct event *event = userdata; + + assert(event); + assert(event->worker); + + kill_and_sigcont(event->worker->pid, arg_timeout_signal); + event->worker->state = WORKER_KILLED; + + log_device_error(event->dev, "Worker ["PID_FMT"] processing SEQNUM=%"PRIu64" killed", event->worker->pid, event->seqnum); + + return 1; +} + +static int on_event_timeout_warning(sd_event_source *s, uint64_t usec, void *userdata) { + struct event *event = userdata; + + assert(event); + assert(event->worker); + + log_device_warning(event->dev, "Worker ["PID_FMT"] processing SEQNUM=%"PRIu64" is taking a long time", event->worker->pid, event->seqnum); + + return 1; +} + +static void worker_attach_event(struct worker *worker, struct event *event) { + sd_event *e; + + assert(worker); + assert(worker->manager); + assert(event); + assert(!event->worker); + assert(!worker->event); + + worker->state = WORKER_RUNNING; + worker->event = event; + event->state = EVENT_RUNNING; + event->worker = worker; + + e = worker->manager->event; + + (void) sd_event_add_time_relative(e, &event->timeout_warning_event, CLOCK_MONOTONIC, + udev_warn_timeout(arg_event_timeout_usec), USEC_PER_SEC, + on_event_timeout_warning, event); + + (void) sd_event_add_time_relative(e, &event->timeout_event, CLOCK_MONOTONIC, + arg_event_timeout_usec, USEC_PER_SEC, + on_event_timeout, event); +} + +static void manager_clear_for_worker(Manager *manager) { + assert(manager); + + manager->inotify_event = sd_event_source_unref(manager->inotify_event); + manager->kill_workers_event = sd_event_source_unref(manager->kill_workers_event); + + manager->event = sd_event_unref(manager->event); + + manager->workers = hashmap_free(manager->workers); + event_queue_cleanup(manager, EVENT_UNDEF); + + manager->monitor = sd_device_monitor_unref(manager->monitor); + manager->ctrl = udev_ctrl_unref(manager->ctrl); + + manager->worker_watch[READ_END] = safe_close(manager->worker_watch[READ_END]); +} + +static void manager_free(Manager *manager) { + if (!manager) + return; + + udev_builtin_exit(); + + if (manager->pid == getpid_cached()) + udev_ctrl_cleanup(manager->ctrl); + + manager_clear_for_worker(manager); + + sd_netlink_unref(manager->rtnl); + + hashmap_free_free_free(manager->properties); + udev_rules_free(manager->rules); + + safe_close(manager->fd_inotify); + safe_close_pair(manager->worker_watch); + + free(manager); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +static int worker_send_message(int fd) { + struct worker_message message = {}; + + return loop_write(fd, &message, sizeof(message), false); +} + +static int worker_lock_block_device(sd_device *dev, int *ret_fd) { + _cleanup_close_ int fd = -1; + const char *val; + int r; + + assert(dev); + assert(ret_fd); + + /* + * Take a shared lock on the device node; this establishes + * a concept of device "ownership" to serialize device + * access. External processes holding an exclusive lock will + * cause udev to skip the event handling; in the case udev + * acquired the lock, the external process can block until + * udev has finished its event handling. + */ + + if (device_for_action(dev, DEVICE_ACTION_REMOVE)) + return 0; + + r = sd_device_get_subsystem(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get subsystem: %m"); + + if (!streq(val, "block")) + return 0; + + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + if (STARTSWITH_SET(val, "dm-", "md", "drbd")) + return 0; + + r = sd_device_get_devtype(dev, &val); + if (r < 0 && r != -ENOENT) + return log_device_debug_errno(dev, r, "Failed to get devtype: %m"); + if (r >= 0 && streq(val, "partition")) { + r = sd_device_get_parent(dev, &dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get parent device: %m"); + } + + r = sd_device_get_devname(dev, &val); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devname: %m"); + + fd = open(val, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK); + if (fd < 0) { + log_device_debug_errno(dev, errno, "Failed to open '%s', ignoring: %m", val); + return 0; + } + + if (flock(fd, LOCK_SH|LOCK_NB) < 0) + return log_device_debug_errno(dev, errno, "Failed to flock(%s): %m", val); + + *ret_fd = TAKE_FD(fd); + return 1; +} + +static int worker_mark_block_device_read_only(sd_device *dev) { + _cleanup_close_ int fd = -1; + const char *val; + int state = 1, r; + + assert(dev); + + if (!arg_blockdev_read_only) + return 0; + + /* Do this only once, when the block device is new. If the device is later retriggered let's not + * toggle the bit again, so that people can boot up with full read-only mode and then unset the bit + * for specific devices only. */ + if (!device_for_action(dev, DEVICE_ACTION_ADD)) + return 0; + + r = sd_device_get_subsystem(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get subsystem: %m"); + + if (!streq(val, "block")) + return 0; + + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get sysname: %m"); + + /* Exclude synthetic devices for now, this is supposed to be a safety feature to avoid modification + * of physical devices, and what sits on top of those doesn't really matter if we don't allow the + * underlying block devices to receive changes. */ + if (STARTSWITH_SET(val, "dm-", "md", "drbd", "loop", "nbd", "zram")) + return 0; + + r = sd_device_get_devname(dev, &val); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get devname: %m"); + + fd = open(val, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK); + if (fd < 0) + return log_device_debug_errno(dev, errno, "Failed to open '%s', ignoring: %m", val); + + if (ioctl(fd, BLKROSET, &state) < 0) + return log_device_warning_errno(dev, errno, "Failed to mark block device '%s' read-only: %m", val); + + log_device_info(dev, "Successfully marked block device '%s' read-only.", val); + return 0; +} + +static int worker_process_device(Manager *manager, sd_device *dev) { + _cleanup_(udev_event_freep) UdevEvent *udev_event = NULL; + _cleanup_close_ int fd_lock = -1; + DeviceAction action; + uint64_t seqnum; + int r; + + assert(manager); + assert(dev); + + r = device_get_seqnum(dev, &seqnum); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get SEQNUM: %m"); + + r = device_get_action(dev, &action); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get ACTION: %m"); + + log_device_debug(dev, "Processing device (SEQNUM=%"PRIu64", ACTION=%s)", + seqnum, device_action_to_string(action)); + + udev_event = udev_event_new(dev, arg_exec_delay_usec, manager->rtnl); + if (!udev_event) + return -ENOMEM; + + r = worker_lock_block_device(dev, &fd_lock); + if (r == -EAGAIN) { + /* So this is a block device and the device is locked currently via the BSD advisory locks — + * someone else is exclusively using it. This means we don't run our udev rules now, to not + * interfere. However we want to know when the device is unlocked again, and retrigger the + * device again then, so that the rules are run eventually. For that we use IN_CLOSE_WRITE + * inotify watches (which isn't exactly the same as waiting for the BSD locks to release, but + * not totally off, as long as unlock+close() is done together, as it usually is). + * + * (The user-facing side of this: https://systemd.io/BLOCK_DEVICE_LOCKING) + * + * There's a bit of a chicken and egg problem here for this however: inotify watching is + * supposed to be enabled via an option set via udev rules (OPTIONS+="watch"). If we skip the + * udev rules here however (as we just said we do), we would thus never see that specific + * udev rule, and thus never turn on inotify watching. But in order to catch up eventually + * and run them we we need the inotify watching: hence a classic chicken and egg problem. + * + * Our way out here: if we see the block device locked, unconditionally watch the device via + * inotify, regardless of any explicit request via OPTIONS+="watch". Thus, a device that is + * currently locked via the BSD file locks will be treated as if we ran a single udev rule + * only for it: the one that turns on inotify watching for it. If we eventually see the + * inotify IN_CLOSE_WRITE event, and then run the rules after all and we then realize that + * this wasn't actually requested (i.e. no OPTIONS+="watch" set) we'll simply turn off the + * watching again (see below). Effectively this means: inotify watching is now enabled either + * a) when the udev rules say so, or b) while the device is locked. + * + * Worst case scenario hence: in the (unlikely) case someone locked the device and we clash + * with that we might do inotify watching for a brief moment for a device where we actually + * weren't supposed to. But that shouldn't be too bad, in particular as BSD locks being taken + * on a block device is kinda an indication that the inotify logic is desired too, to some + * degree — they go hand-in-hand after all. */ + + log_device_debug(dev, "Block device is currently locked, installing watch to wait until the lock is released."); + (void) udev_watch_begin(dev); + + /* Now the watch is installed, let's lock the device again, maybe in the meantime things changed */ + r = worker_lock_block_device(dev, &fd_lock); + } + if (r < 0) + return r; + + (void) worker_mark_block_device_read_only(dev); + + /* apply rules, create node, symlinks */ + r = udev_event_execute_rules(udev_event, arg_event_timeout_usec, arg_timeout_signal, manager->properties, manager->rules); + if (r < 0) + return r; + + udev_event_execute_run(udev_event, arg_event_timeout_usec, arg_timeout_signal); + + if (!manager->rtnl) + /* in case rtnl was initialized */ + manager->rtnl = sd_netlink_ref(udev_event->rtnl); + + /* apply/restore/end inotify watch */ + if (udev_event->inotify_watch) { + (void) udev_watch_begin(dev); + r = device_update_db(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m"); + } else + (void) udev_watch_end(dev); + + log_device_debug(dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) processed", + seqnum, device_action_to_string(action)); + + return 0; +} + +static int worker_device_monitor_handler(sd_device_monitor *monitor, sd_device *dev, void *userdata) { + Manager *manager = userdata; + int r; + + assert(dev); + assert(manager); + + r = worker_process_device(manager, dev); + if (r == -EAGAIN) + /* if we couldn't acquire the flock(), then proceed quietly */ + log_device_debug_errno(dev, r, "Device currently locked, not processing."); + else { + if (r < 0) + log_device_warning_errno(dev, r, "Failed to process device, ignoring: %m"); + + /* send processed event back to libudev listeners */ + r = device_monitor_send_device(monitor, NULL, dev); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to send device, ignoring: %m"); + } + + /* send udevd the result of the event execution */ + r = worker_send_message(manager->worker_watch[WRITE_END]); + if (r < 0) + log_device_warning_errno(dev, r, "Failed to send signal to main daemon, ignoring: %m"); + + return 1; +} + +static int worker_main(Manager *_manager, sd_device_monitor *monitor, sd_device *first_device) { + _cleanup_(sd_device_unrefp) sd_device *dev = first_device; + _cleanup_(manager_freep) Manager *manager = _manager; + int r; + + assert(manager); + assert(monitor); + assert(dev); + + assert_se(unsetenv("NOTIFY_SOCKET") == 0); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, -1) >= 0); + + /* Reset OOM score, we only protect the main daemon. */ + r = set_oom_score_adjust(0); + if (r < 0) + log_debug_errno(r, "Failed to reset OOM score, ignoring: %m"); + + /* Clear unnecessary data in Manager object.*/ + manager_clear_for_worker(manager); + + r = sd_event_new(&manager->event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGTERM, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to set SIGTERM event: %m"); + + r = sd_device_monitor_attach_event(monitor, manager->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event loop to device monitor: %m"); + + r = sd_device_monitor_start(monitor, worker_device_monitor_handler, manager); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + (void) sd_event_source_set_description(sd_device_monitor_get_event_source(monitor), "worker-device-monitor"); + + /* Process first device */ + (void) worker_device_monitor_handler(monitor, dev, manager); + + r = sd_event_loop(manager->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +static int worker_spawn(Manager *manager, struct event *event) { + _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *worker_monitor = NULL; + struct worker *worker; + pid_t pid; + int r; + + /* listen for new events */ + r = device_monitor_new_full(&worker_monitor, MONITOR_GROUP_NONE, -1); + if (r < 0) + return r; + + /* allow the main daemon netlink address to send devices to the worker */ + r = device_monitor_allow_unicast_sender(worker_monitor, manager->monitor); + if (r < 0) + return log_error_errno(r, "Worker: Failed to set unicast sender: %m"); + + r = device_monitor_enable_receiving(worker_monitor); + if (r < 0) + return log_error_errno(r, "Worker: Failed to enable receiving of device: %m"); + + r = safe_fork(NULL, FORK_DEATHSIG, &pid); + if (r < 0) { + event->state = EVENT_QUEUED; + return log_error_errno(r, "Failed to fork() worker: %m"); + } + if (r == 0) { + /* Worker process */ + r = worker_main(manager, worker_monitor, sd_device_ref(event->dev)); + log_close(); + _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); + } + + r = worker_new(&worker, manager, worker_monitor, pid); + if (r < 0) + return log_error_errno(r, "Failed to create worker object: %m"); + + worker_attach_event(worker, event); + + log_device_debug(event->dev, "Worker ["PID_FMT"] is forked for processing SEQNUM=%"PRIu64".", pid, event->seqnum); + return 0; +} + +static void event_run(Manager *manager, struct event *event) { + static bool log_children_max_reached = true; + struct worker *worker; + int r; + + assert(manager); + assert(event); + + if (DEBUG_LOGGING) { + DeviceAction action; + + r = device_get_action(event->dev, &action); + log_device_debug(event->dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) ready for processing", + event->seqnum, r >= 0 ? device_action_to_string(action) : "<unknown>"); + } + + HASHMAP_FOREACH(worker, manager->workers) { + if (worker->state != WORKER_IDLE) + continue; + + r = device_monitor_send_device(manager->monitor, worker->monitor, event->dev); + if (r < 0) { + log_device_error_errno(event->dev, r, "Worker ["PID_FMT"] did not accept message, killing the worker: %m", + worker->pid); + (void) kill(worker->pid, SIGKILL); + worker->state = WORKER_KILLED; + continue; + } + worker_attach_event(worker, event); + return; + } + + if (hashmap_size(manager->workers) >= arg_children_max) { + + /* Avoid spamming the debug logs if the limit is already reached and + * many events still need to be processed */ + if (log_children_max_reached && arg_children_max > 1) { + log_debug("Maximum number (%u) of children reached.", hashmap_size(manager->workers)); + log_children_max_reached = false; + } + return; + } + + /* Re-enable the debug message for the next batch of events */ + log_children_max_reached = true; + + /* fork with up-to-date SELinux label database, so the child inherits the up-to-date db + and, until the next SELinux policy changes, we safe further reloads in future children */ + mac_selinux_maybe_reload(); + + /* start new worker and pass initial device */ + worker_spawn(manager, event); +} + +static int event_queue_insert(Manager *manager, sd_device *dev) { + _cleanup_(sd_device_unrefp) sd_device *clone = NULL; + struct event *event; + DeviceAction action; + uint64_t seqnum; + int r; + + assert(manager); + assert(dev); + + /* only one process can add events to the queue */ + assert(manager->pid == getpid_cached()); + + /* We only accepts devices received by device monitor. */ + r = device_get_seqnum(dev, &seqnum); + if (r < 0) + return r; + + /* Refuse devices do not have ACTION property. */ + r = device_get_action(dev, &action); + if (r < 0) + return r; + + /* Save original device to restore the state on failures. */ + r = device_shallow_clone(dev, &clone); + if (r < 0) + return r; + + r = device_copy_properties(clone, dev); + if (r < 0) + return r; + + event = new(struct event, 1); + if (!event) + return -ENOMEM; + + *event = (struct event) { + .manager = manager, + .dev = sd_device_ref(dev), + .dev_kernel = TAKE_PTR(clone), + .seqnum = seqnum, + .state = EVENT_QUEUED, + }; + + if (LIST_IS_EMPTY(manager->events)) { + r = touch("/run/udev/queue"); + if (r < 0) + log_warning_errno(r, "Failed to touch /run/udev/queue: %m"); + } + + LIST_APPEND(event, manager->events, event); + + log_device_debug(dev, "Device (SEQNUM=%"PRIu64", ACTION=%s) is queued", + seqnum, device_action_to_string(action)); + + return 0; +} + +static void manager_kill_workers(Manager *manager) { + struct worker *worker; + + assert(manager); + + HASHMAP_FOREACH(worker, manager->workers) { + if (worker->state == WORKER_KILLED) + continue; + + worker->state = WORKER_KILLED; + (void) kill(worker->pid, SIGTERM); + } +} + +/* lookup event for identical, parent, child device */ +static int is_device_busy(Manager *manager, struct event *event) { + const char *subsystem, *devpath, *devpath_old = NULL; + dev_t devnum = makedev(0, 0); + struct event *loop_event; + size_t devpath_len; + int r, ifindex = 0; + bool is_block; + + r = sd_device_get_subsystem(event->dev, &subsystem); + if (r < 0) + return r; + + is_block = streq(subsystem, "block"); + + r = sd_device_get_devpath(event->dev, &devpath); + if (r < 0) + return r; + + devpath_len = strlen(devpath); + + r = sd_device_get_property_value(event->dev, "DEVPATH_OLD", &devpath_old); + if (r < 0 && r != -ENOENT) + return r; + + r = sd_device_get_devnum(event->dev, &devnum); + if (r < 0 && r != -ENOENT) + return r; + + r = sd_device_get_ifindex(event->dev, &ifindex); + if (r < 0 && r != -ENOENT) + return r; + + /* check if queue contains events we depend on */ + LIST_FOREACH(event, loop_event, manager->events) { + size_t loop_devpath_len, common; + const char *loop_devpath; + + /* we already found a later event, earlier cannot block us, no need to check again */ + if (loop_event->seqnum < event->delaying_seqnum) + continue; + + /* event we checked earlier still exists, no need to check again */ + if (loop_event->seqnum == event->delaying_seqnum) + return true; + + /* found ourself, no later event can block us */ + if (loop_event->seqnum >= event->seqnum) + break; + + /* check major/minor */ + if (major(devnum) != 0) { + const char *s; + dev_t d; + + if (sd_device_get_subsystem(loop_event->dev, &s) < 0) + continue; + + if (sd_device_get_devnum(loop_event->dev, &d) >= 0 && + devnum == d && is_block == streq(s, "block")) + goto set_delaying_seqnum; + } + + /* check network device ifindex */ + if (ifindex > 0) { + int i; + + if (sd_device_get_ifindex(loop_event->dev, &i) >= 0 && + ifindex == i) + goto set_delaying_seqnum; + } + + if (sd_device_get_devpath(loop_event->dev, &loop_devpath) < 0) + continue; + + /* check our old name */ + if (devpath_old && streq(devpath_old, loop_devpath)) + goto set_delaying_seqnum; + + loop_devpath_len = strlen(loop_devpath); + + /* compare devpath */ + common = MIN(devpath_len, loop_devpath_len); + + /* one devpath is contained in the other? */ + if (!strneq(devpath, loop_devpath, common)) + continue; + + /* identical device event found */ + if (devpath_len == loop_devpath_len) + goto set_delaying_seqnum; + + /* parent device event found */ + if (devpath[common] == '/') + goto set_delaying_seqnum; + + /* child device event found */ + if (loop_devpath[common] == '/') + goto set_delaying_seqnum; + } + + return false; + +set_delaying_seqnum: + log_device_debug(event->dev, "SEQNUM=%" PRIu64 " blocked by SEQNUM=%" PRIu64, + event->seqnum, loop_event->seqnum); + + event->delaying_seqnum = loop_event->seqnum; + return true; +} + +static void manager_exit(Manager *manager) { + assert(manager); + + manager->exit = true; + + sd_notify(false, + "STOPPING=1\n" + "STATUS=Starting shutdown..."); + + /* close sources of new events and discard buffered events */ + manager->ctrl = udev_ctrl_unref(manager->ctrl); + + manager->inotify_event = sd_event_source_unref(manager->inotify_event); + manager->fd_inotify = safe_close(manager->fd_inotify); + + manager->monitor = sd_device_monitor_unref(manager->monitor); + + /* discard queued events and kill workers */ + event_queue_cleanup(manager, EVENT_QUEUED); + manager_kill_workers(manager); +} + +/* reload requested, HUP signal received, rules changed, builtin changed */ +static void manager_reload(Manager *manager) { + + assert(manager); + + sd_notify(false, + "RELOADING=1\n" + "STATUS=Flushing configuration..."); + + manager_kill_workers(manager); + manager->rules = udev_rules_free(manager->rules); + udev_builtin_exit(); + + sd_notifyf(false, + "READY=1\n" + "STATUS=Processing with %u children at max", arg_children_max); +} + +static int on_kill_workers_event(sd_event_source *s, uint64_t usec, void *userdata) { + Manager *manager = userdata; + + assert(manager); + + log_debug("Cleanup idle workers"); + manager_kill_workers(manager); + + return 1; +} + +static void event_queue_start(Manager *manager) { + struct event *event; + usec_t usec; + int r; + + assert(manager); + + if (LIST_IS_EMPTY(manager->events) || + manager->exit || manager->stop_exec_queue) + return; + + assert_se(sd_event_now(manager->event, CLOCK_MONOTONIC, &usec) >= 0); + /* check for changed config, every 3 seconds at most */ + if (manager->last_usec == 0 || + usec - manager->last_usec > 3 * USEC_PER_SEC) { + if (udev_rules_check_timestamp(manager->rules) || + udev_builtin_validate()) + manager_reload(manager); + + manager->last_usec = usec; + } + + r = event_source_disable(manager->kill_workers_event); + if (r < 0) + log_warning_errno(r, "Failed to disable event source for cleaning up idle workers, ignoring: %m"); + + udev_builtin_init(); + + if (!manager->rules) { + r = udev_rules_load(&manager->rules, arg_resolve_name_timing); + if (r < 0) { + log_warning_errno(r, "Failed to read udev rules: %m"); + return; + } + } + + LIST_FOREACH(event, event, manager->events) { + if (event->state != EVENT_QUEUED) + continue; + + /* do not start event if parent or child event is still running */ + if (is_device_busy(manager, event) != 0) + continue; + + event_run(manager, event); + } +} + +static void event_queue_cleanup(Manager *manager, enum event_state match_type) { + struct event *event, *tmp; + + LIST_FOREACH_SAFE(event, event, tmp, manager->events) { + if (match_type != EVENT_UNDEF && match_type != event->state) + continue; + + event_free(event); + } +} + +static int on_worker(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *manager = userdata; + + assert(manager); + + for (;;) { + struct worker_message msg; + struct iovec iovec = { + .iov_base = &msg, + .iov_len = sizeof(msg), + }; + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; + struct msghdr msghdr = { + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + ssize_t size; + struct ucred *ucred; + struct worker *worker; + + size = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT); + if (size == -EINTR) + continue; + if (size == -EAGAIN) + /* nothing more to read */ + break; + if (size < 0) + return log_error_errno(size, "Failed to receive message: %m"); + + cmsg_close_all(&msghdr); + + if (size != sizeof(struct worker_message)) { + log_warning("Ignoring worker message with invalid size %zi bytes", size); + continue; + } + + ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred); + if (!ucred || ucred->pid <= 0) { + log_warning("Ignoring worker message without valid PID"); + continue; + } + + /* lookup worker who sent the signal */ + worker = hashmap_get(manager->workers, PID_TO_PTR(ucred->pid)); + if (!worker) { + log_debug("Worker ["PID_FMT"] returned, but is no longer tracked", ucred->pid); + continue; + } + + if (worker->state != WORKER_KILLED) + worker->state = WORKER_IDLE; + + /* worker returned */ + event_free(worker->event); + } + + /* we have free workers, try to schedule events */ + event_queue_start(manager); + + return 1; +} + +static int on_uevent(sd_device_monitor *monitor, sd_device *dev, void *userdata) { + Manager *manager = userdata; + int r; + + assert(manager); + + device_ensure_usec_initialized(dev, NULL); + + r = event_queue_insert(manager, dev); + if (r < 0) { + log_device_error_errno(dev, r, "Failed to insert device into event queue: %m"); + return 1; + } + + /* we have fresh events, try to schedule them */ + event_queue_start(manager); + + return 1; +} + +/* receive the udevd message from userspace */ +static int on_ctrl_msg(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, const union udev_ctrl_msg_value *value, void *userdata) { + Manager *manager = userdata; + int r; + + assert(value); + assert(manager); + + switch (type) { + case UDEV_CTRL_SET_LOG_LEVEL: + log_debug("Received udev control message (SET_LOG_LEVEL), setting log_level=%i", value->intval); + log_set_max_level_realm(LOG_REALM_UDEV, value->intval); + log_set_max_level_realm(LOG_REALM_SYSTEMD, value->intval); + manager_kill_workers(manager); + break; + case UDEV_CTRL_STOP_EXEC_QUEUE: + log_debug("Received udev control message (STOP_EXEC_QUEUE)"); + manager->stop_exec_queue = true; + break; + case UDEV_CTRL_START_EXEC_QUEUE: + log_debug("Received udev control message (START_EXEC_QUEUE)"); + manager->stop_exec_queue = false; + event_queue_start(manager); + break; + case UDEV_CTRL_RELOAD: + log_debug("Received udev control message (RELOAD)"); + manager_reload(manager); + break; + case UDEV_CTRL_SET_ENV: { + _cleanup_free_ char *key = NULL, *val = NULL, *old_key = NULL, *old_val = NULL; + const char *eq; + + eq = strchr(value->buf, '='); + if (!eq) { + log_error("Invalid key format '%s'", value->buf); + return 1; + } + + key = strndup(value->buf, eq - value->buf); + if (!key) { + log_oom(); + return 1; + } + + old_val = hashmap_remove2(manager->properties, key, (void **) &old_key); + + r = hashmap_ensure_allocated(&manager->properties, &string_hash_ops); + if (r < 0) { + log_oom(); + return 1; + } + + eq++; + if (isempty(eq)) { + log_debug("Received udev control message (ENV), unsetting '%s'", key); + + r = hashmap_put(manager->properties, key, NULL); + if (r < 0) { + log_oom(); + return 1; + } + } else { + val = strdup(eq); + if (!val) { + log_oom(); + return 1; + } + + log_debug("Received udev control message (ENV), setting '%s=%s'", key, val); + + r = hashmap_put(manager->properties, key, val); + if (r < 0) { + log_oom(); + return 1; + } + } + + key = val = NULL; + manager_kill_workers(manager); + break; + } + case UDEV_CTRL_SET_CHILDREN_MAX: + if (value->intval <= 0) { + log_debug("Received invalid udev control message (SET_MAX_CHILDREN, %i), ignoring.", value->intval); + return 0; + } + + log_debug("Received udev control message (SET_MAX_CHILDREN), setting children_max=%i", value->intval); + arg_children_max = value->intval; + + (void) sd_notifyf(false, + "READY=1\n" + "STATUS=Processing with %u children at max", arg_children_max); + break; + case UDEV_CTRL_PING: + log_debug("Received udev control message (PING)"); + break; + case UDEV_CTRL_EXIT: + log_debug("Received udev control message (EXIT)"); + manager_exit(manager); + break; + default: + log_debug("Received unknown udev control message, ignoring"); + } + + return 1; +} + +static int synthesize_change_one(sd_device *dev, const char *syspath) { + const char *filename; + int r; + + filename = strjoina(syspath, "/uevent"); + log_device_debug(dev, "device is closed, synthesising 'change' on %s", syspath); + r = write_string_file(filename, "change", WRITE_STRING_FILE_DISABLE_BUFFER); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to write 'change' to %s: %m", filename); + return 0; +} + +static int synthesize_change(sd_device *dev) { + const char *subsystem, *sysname, *devname, *syspath, *devtype; + int r; + + r = sd_device_get_subsystem(dev, &subsystem); + if (r < 0) + return r; + + r = sd_device_get_sysname(dev, &sysname); + if (r < 0) + return r; + + r = sd_device_get_devname(dev, &devname); + if (r < 0) + return r; + + r = sd_device_get_syspath(dev, &syspath); + if (r < 0) + return r; + + r = sd_device_get_devtype(dev, &devtype); + if (r < 0) + return r; + + if (streq_ptr("block", subsystem) && + streq_ptr("disk", devtype) && + !startswith(sysname, "dm-")) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + bool part_table_read = false, has_partitions = false; + sd_device *d; + int fd; + + /* + * Try to re-read the partition table. This only succeeds if + * none of the devices is busy. The kernel returns 0 if no + * partition table is found, and we will not get an event for + * the disk. + */ + fd = open(devname, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK); + if (fd >= 0) { + r = flock(fd, LOCK_EX|LOCK_NB); + if (r >= 0) + r = ioctl(fd, BLKRRPART, 0); + + close(fd); + if (r >= 0) + part_table_read = true; + } + + /* search for partitions */ + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_parent(e, dev); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, "block", true); + if (r < 0) + return r; + + FOREACH_DEVICE(e, d) { + const char *t; + + if (sd_device_get_devtype(d, &t) < 0 || + !streq("partition", t)) + continue; + + has_partitions = true; + break; + } + + /* + * We have partitions and re-read the table, the kernel already sent + * out a "change" event for the disk, and "remove/add" for all + * partitions. + */ + if (part_table_read && has_partitions) + return 0; + + /* + * We have partitions but re-reading the partition table did not + * work, synthesize "change" for the disk and all partitions. + */ + (void) synthesize_change_one(dev, syspath); + + FOREACH_DEVICE(e, d) { + const char *t, *n, *s; + + if (sd_device_get_devtype(d, &t) < 0 || + !streq("partition", t)) + continue; + + if (sd_device_get_devname(d, &n) < 0 || + sd_device_get_syspath(d, &s) < 0) + continue; + + (void) synthesize_change_one(dev, s); + } + + } else + (void) synthesize_change_one(dev, syspath); + + return 0; +} + +static int on_inotify(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *manager = userdata; + union inotify_event_buffer buffer; + struct inotify_event *e; + ssize_t l; + int r; + + assert(manager); + + r = event_source_disable(manager->kill_workers_event); + if (r < 0) + log_warning_errno(r, "Failed to disable event source for cleaning up idle workers, ignoring: %m"); + + l = read(fd, &buffer, sizeof(buffer)); + if (l < 0) { + if (IN_SET(errno, EAGAIN, EINTR)) + return 1; + + return log_error_errno(errno, "Failed to read inotify fd: %m"); + } + + FOREACH_INOTIFY_EVENT(e, buffer, l) { + _cleanup_(sd_device_unrefp) sd_device *dev = NULL; + const char *devnode; + + if (udev_watch_lookup(e->wd, &dev) <= 0) + continue; + + if (sd_device_get_devname(dev, &devnode) < 0) + continue; + + log_device_debug(dev, "Inotify event: %x for %s", e->mask, devnode); + if (e->mask & IN_CLOSE_WRITE) + synthesize_change(dev); + else if (e->mask & IN_IGNORED) + udev_watch_end(dev); + } + + return 1; +} + +static int on_sigterm(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *manager = userdata; + + assert(manager); + + manager_exit(manager); + + return 1; +} + +static int on_sighup(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *manager = userdata; + + assert(manager); + + manager_reload(manager); + + return 1; +} + +static int on_sigchld(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *manager = userdata; + int r; + + assert(manager); + + for (;;) { + pid_t pid; + int status; + struct worker *worker; + + pid = waitpid(-1, &status, WNOHANG); + if (pid <= 0) + break; + + worker = hashmap_get(manager->workers, PID_TO_PTR(pid)); + if (!worker) { + log_warning("Worker ["PID_FMT"] is unknown, ignoring", pid); + continue; + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == 0) + log_debug("Worker ["PID_FMT"] exited", pid); + else + log_warning("Worker ["PID_FMT"] exited with return code %i", pid, WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) + log_warning("Worker ["PID_FMT"] terminated by signal %i (%s)", pid, WTERMSIG(status), signal_to_string(WTERMSIG(status))); + else if (WIFSTOPPED(status)) { + log_info("Worker ["PID_FMT"] stopped", pid); + continue; + } else if (WIFCONTINUED(status)) { + log_info("Worker ["PID_FMT"] continued", pid); + continue; + } else + log_warning("Worker ["PID_FMT"] exit with status 0x%04x", pid, status); + + if ((!WIFEXITED(status) || WEXITSTATUS(status) != 0) && worker->event) { + log_device_error(worker->event->dev, "Worker ["PID_FMT"] failed", pid); + + /* delete state from disk */ + device_delete_db(worker->event->dev); + device_tag_index(worker->event->dev, NULL, false); + + if (manager->monitor) { + /* forward kernel event without amending it */ + r = device_monitor_send_device(manager->monitor, NULL, worker->event->dev_kernel); + if (r < 0) + log_device_error_errno(worker->event->dev_kernel, r, "Failed to send back device to kernel: %m"); + } + } + + worker_free(worker); + } + + /* we can start new workers, try to schedule events */ + event_queue_start(manager); + + /* Disable unnecessary cleanup event */ + if (hashmap_isempty(manager->workers)) { + r = event_source_disable(manager->kill_workers_event); + if (r < 0) + log_warning_errno(r, "Failed to disable event source for cleaning up idle workers, ignoring: %m"); + } + + return 1; +} + +static int on_post(sd_event_source *s, void *userdata) { + Manager *manager = userdata; + + assert(manager); + + if (!LIST_IS_EMPTY(manager->events)) + return 1; + + /* There are no pending events. Let's cleanup idle process. */ + + if (!hashmap_isempty(manager->workers)) { + /* There are idle workers */ + (void) event_reset_time(manager->event, &manager->kill_workers_event, CLOCK_MONOTONIC, + now(CLOCK_MONOTONIC) + 3 * USEC_PER_SEC, USEC_PER_SEC, + on_kill_workers_event, manager, 0, "kill-workers-event", false); + return 1; + } + + /* There are no idle workers. */ + + if (manager->exit) + return sd_event_exit(manager->event, 0); + + if (manager->cgroup) + /* cleanup possible left-over processes in our cgroup */ + (void) cg_kill(SYSTEMD_CGROUP_CONTROLLER, manager->cgroup, SIGKILL, CGROUP_IGNORE_SELF, NULL, NULL, NULL); + + return 1; +} + +static int listen_fds(int *ret_ctrl, int *ret_netlink) { + int ctrl_fd = -1, netlink_fd = -1; + int fd, n; + + assert(ret_ctrl); + assert(ret_netlink); + + n = sd_listen_fds(true); + if (n < 0) + return n; + + for (fd = SD_LISTEN_FDS_START; fd < n + SD_LISTEN_FDS_START; fd++) { + if (sd_is_socket(fd, AF_LOCAL, SOCK_SEQPACKET, -1) > 0) { + if (ctrl_fd >= 0) + return -EINVAL; + ctrl_fd = fd; + continue; + } + + if (sd_is_socket(fd, AF_NETLINK, SOCK_RAW, -1) > 0) { + if (netlink_fd >= 0) + return -EINVAL; + netlink_fd = fd; + continue; + } + + return -EINVAL; + } + + *ret_ctrl = ctrl_fd; + *ret_netlink = netlink_fd; + + return 0; +} + +/* + * read the kernel command line, in case we need to get into debug mode + * udev.log_level=<level> syslog priority + * udev.children_max=<number of workers> events are fully serialized if set to 1 + * udev.exec_delay=<number of seconds> delay execution of every executed program + * udev.event_timeout=<number of seconds> seconds to wait before terminating an event + * udev.blockdev_read_only<=bool> mark all block devices read-only when they appear + */ +static int parse_proc_cmdline_item(const char *key, const char *value, void *data) { + int r; + + assert(key); + + if (proc_cmdline_key_streq(key, "udev.log_level") || + proc_cmdline_key_streq(key, "udev.log_priority")) { /* kept for backward compatibility */ + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = log_level_from_string(value); + if (r >= 0) + log_set_max_level(r); + + } else if (proc_cmdline_key_streq(key, "udev.event_timeout")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = parse_sec(value, &arg_event_timeout_usec); + + } else if (proc_cmdline_key_streq(key, "udev.children_max")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = safe_atou(value, &arg_children_max); + + } else if (proc_cmdline_key_streq(key, "udev.exec_delay")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = parse_sec(value, &arg_exec_delay_usec); + + } else if (proc_cmdline_key_streq(key, "udev.timeout_signal")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = signal_from_string(value); + if (r > 0) + arg_timeout_signal = r; + + } else if (proc_cmdline_key_streq(key, "udev.blockdev_read_only")) { + + if (!value) + arg_blockdev_read_only = true; + else { + r = parse_boolean(value); + if (r < 0) + log_warning_errno(r, "Failed to parse udev.blockdev-read-only argument, ignoring: %s", value); + else + arg_blockdev_read_only = r; + } + + if (arg_blockdev_read_only) + log_notice("All physical block devices will be marked read-only."); + + return 0; + + } else { + if (startswith(key, "udev.")) + log_warning("Unknown udev kernel command line option \"%s\", ignoring.", key); + + return 0; + } + + if (r < 0) + log_warning_errno(r, "Failed to parse \"%s=%s\", ignoring: %m", key, value); + + return 0; +} + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-udevd.service", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...]\n\n" + "Rule-based manager for device events and files.\n\n" + " -h --help Print this message\n" + " -V --version Print version of the program\n" + " -d --daemon Detach and run in the background\n" + " -D --debug Enable debug output\n" + " -c --children-max=INT Set maximum number of workers\n" + " -e --exec-delay=SECONDS Seconds to wait before executing RUN=\n" + " -t --event-timeout=SECONDS Seconds to wait before terminating an event\n" + " -N --resolve-names=early|late|never\n" + " When to resolve users and groups\n" + "\nSee the %s for details.\n" + , program_invocation_short_name + , link + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_TIMEOUT_SIGNAL, + }; + + static const struct option options[] = { + { "daemon", no_argument, NULL, 'd' }, + { "debug", no_argument, NULL, 'D' }, + { "children-max", required_argument, NULL, 'c' }, + { "exec-delay", required_argument, NULL, 'e' }, + { "event-timeout", required_argument, NULL, 't' }, + { "resolve-names", required_argument, NULL, 'N' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "timeout-signal", required_argument, NULL, ARG_TIMEOUT_SIGNAL }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "c:de:Dt:N:hV", options, NULL)) >= 0) { + switch (c) { + + case 'd': + arg_daemonize = true; + break; + case 'c': + r = safe_atou(optarg, &arg_children_max); + if (r < 0) + log_warning_errno(r, "Failed to parse --children-max= value '%s', ignoring: %m", optarg); + break; + case 'e': + r = parse_sec(optarg, &arg_exec_delay_usec); + if (r < 0) + log_warning_errno(r, "Failed to parse --exec-delay= value '%s', ignoring: %m", optarg); + break; + case ARG_TIMEOUT_SIGNAL: + r = signal_from_string(optarg); + if (r <= 0) + log_warning_errno(r, "Failed to parse --timeout-signal= value '%s', ignoring: %m", optarg); + else + arg_timeout_signal = r; + + break; + case 't': + r = parse_sec(optarg, &arg_event_timeout_usec); + if (r < 0) + log_warning_errno(r, "Failed to parse --event-timeout= value '%s', ignoring: %m", optarg); + break; + case 'D': + arg_debug = true; + break; + case 'N': { + ResolveNameTiming t; + + t = resolve_name_timing_from_string(optarg); + if (t < 0) + log_warning("Invalid --resolve-names= value '%s', ignoring.", optarg); + else + arg_resolve_name_timing = t; + break; + } + case 'h': + return help(); + case 'V': + printf("%s\n", GIT_VERSION); + return 0; + case '?': + return -EINVAL; + default: + assert_not_reached("Unhandled option"); + + } + } + + return 1; +} + +static int manager_new(Manager **ret, int fd_ctrl, int fd_uevent, const char *cgroup) { + _cleanup_(manager_freep) Manager *manager = NULL; + int r; + + assert(ret); + + manager = new(Manager, 1); + if (!manager) + return log_oom(); + + *manager = (Manager) { + .fd_inotify = -1, + .worker_watch = { -1, -1 }, + .cgroup = cgroup, + }; + + r = udev_ctrl_new_from_fd(&manager->ctrl, fd_ctrl); + if (r < 0) + return log_error_errno(r, "Failed to initialize udev control socket: %m"); + + r = udev_ctrl_enable_receiving(manager->ctrl); + if (r < 0) + return log_error_errno(r, "Failed to bind udev control socket: %m"); + + r = device_monitor_new_full(&manager->monitor, MONITOR_GROUP_KERNEL, fd_uevent); + if (r < 0) + return log_error_errno(r, "Failed to initialize device monitor: %m"); + + /* Bump receiver buffer, but only if we are not called via socket activation, as in that + * case systemd sets the receive buffer size for us, and the value in the .socket unit + * should take full effect. */ + if (fd_uevent < 0) { + r = sd_device_monitor_set_receive_buffer_size(manager->monitor, 128 * 1024 * 1024); + if (r < 0) + log_warning_errno(r, "Failed to set receive buffer size for device monitor, ignoring: %m"); + } + + r = device_monitor_enable_receiving(manager->monitor); + if (r < 0) + return log_error_errno(r, "Failed to bind netlink socket: %m"); + + *ret = TAKE_PTR(manager); + + return 0; +} + +static int main_loop(Manager *manager) { + int fd_worker, r; + + manager->pid = getpid_cached(); + + /* unnamed socket from workers to the main daemon */ + r = socketpair(AF_LOCAL, SOCK_DGRAM|SOCK_CLOEXEC, 0, manager->worker_watch); + if (r < 0) + return log_error_errno(errno, "Failed to create socketpair for communicating with workers: %m"); + + fd_worker = manager->worker_watch[READ_END]; + + r = setsockopt_int(fd_worker, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return log_error_errno(r, "Failed to enable SO_PASSCRED: %m"); + + r = udev_watch_init(); + if (r < 0) + return log_error_errno(r, "Failed to create inotify descriptor: %m"); + manager->fd_inotify = r; + + udev_watch_restore(); + + /* block and listen to all signals on signalfd */ + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGHUP, SIGCHLD, -1) >= 0); + + r = sd_event_default(&manager->event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGINT, on_sigterm, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGINT event source: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGTERM, on_sigterm, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGTERM event source: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGHUP, on_sighup, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGHUP event source: %m"); + + r = sd_event_add_signal(manager->event, NULL, SIGCHLD, on_sigchld, manager); + if (r < 0) + return log_error_errno(r, "Failed to create SIGCHLD event source: %m"); + + r = sd_event_set_watchdog(manager->event, true); + if (r < 0) + return log_error_errno(r, "Failed to create watchdog event source: %m"); + + r = udev_ctrl_attach_event(manager->ctrl, manager->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to udev control: %m"); + + r = udev_ctrl_start(manager->ctrl, on_ctrl_msg, manager); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + /* This needs to be after the inotify and uevent handling, to make sure + * that the ping is send back after fully processing the pending uevents + * (including the synthetic ones we may create due to inotify events). + */ + r = sd_event_source_set_priority(udev_ctrl_get_event_source(manager->ctrl), SD_EVENT_PRIORITY_IDLE); + if (r < 0) + return log_error_errno(r, "Failed to set IDLE event priority for udev control event source: %m"); + + r = sd_event_add_io(manager->event, &manager->inotify_event, manager->fd_inotify, EPOLLIN, on_inotify, manager); + if (r < 0) + return log_error_errno(r, "Failed to create inotify event source: %m"); + + r = sd_device_monitor_attach_event(manager->monitor, manager->event); + if (r < 0) + return log_error_errno(r, "Failed to attach event to device monitor: %m"); + + r = sd_device_monitor_start(manager->monitor, on_uevent, manager); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + (void) sd_event_source_set_description(sd_device_monitor_get_event_source(manager->monitor), "device-monitor"); + + r = sd_event_add_io(manager->event, NULL, fd_worker, EPOLLIN, on_worker, manager); + if (r < 0) + return log_error_errno(r, "Failed to create worker event source: %m"); + + r = sd_event_add_post(manager->event, NULL, on_post, manager); + if (r < 0) + return log_error_errno(r, "Failed to create post event source: %m"); + + udev_builtin_init(); + + r = udev_rules_load(&manager->rules, arg_resolve_name_timing); + if (!manager->rules) + return log_error_errno(r, "Failed to read udev rules: %m"); + + r = udev_rules_apply_static_dev_perms(manager->rules); + if (r < 0) + log_error_errno(r, "Failed to apply permissions on static device nodes: %m"); + + (void) sd_notifyf(false, + "READY=1\n" + "STATUS=Processing with %u children at max", arg_children_max); + + r = sd_event_loop(manager->event); + if (r < 0) + log_error_errno(r, "Event loop failed: %m"); + + sd_notify(false, + "STOPPING=1\n" + "STATUS=Shutting down..."); + return r; +} + +int run_udevd(int argc, char *argv[]) { + _cleanup_free_ char *cgroup = NULL; + _cleanup_(manager_freep) Manager *manager = NULL; + int fd_ctrl = -1, fd_uevent = -1; + int r; + + log_set_target(LOG_TARGET_AUTO); + log_open(); + udev_parse_config_full(&arg_children_max, &arg_exec_delay_usec, &arg_event_timeout_usec, &arg_resolve_name_timing, &arg_timeout_signal); + log_parse_environment(); + log_open(); /* Done again to update after reading configuration. */ + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, PROC_CMDLINE_STRIP_RD_PREFIX); + if (r < 0) + log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m"); + + if (arg_debug) { + log_set_target(LOG_TARGET_CONSOLE); + log_set_max_level(LOG_DEBUG); + } + + log_set_max_level_realm(LOG_REALM_SYSTEMD, log_get_max_level()); + + r = must_be_root(); + if (r < 0) + return r; + + if (arg_children_max == 0) { + unsigned long cpu_limit, mem_limit, cpu_count = 1; + + r = cpus_in_affinity_mask(); + if (r < 0) + log_warning_errno(r, "Failed to determine number of local CPUs, ignoring: %m"); + else + cpu_count = r; + + cpu_limit = cpu_count * 2 + 16; + mem_limit = MAX(physical_memory() / (128UL*1024*1024), 10U); + + arg_children_max = MIN(cpu_limit, mem_limit); + arg_children_max = MIN(WORKER_NUM_MAX, arg_children_max); + + log_debug("Set children_max to %u", arg_children_max); + } + + /* set umask before creating any file/directory */ + umask(022); + + r = mac_selinux_init(); + if (r < 0) + return r; + + r = mkdir_errno_wrapper("/run/udev", 0755); + if (r < 0 && r != -EEXIST) + return log_error_errno(r, "Failed to create /run/udev: %m"); + + if (getppid() == 1 && sd_booted() > 0) { + /* Get our own cgroup, we regularly kill everything udev has left behind. + * We only do this on systemd systems, and only if we are directly spawned + * by PID1. Otherwise we are not guaranteed to have a dedicated cgroup. */ + r = cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, 0, &cgroup); + if (r < 0) { + if (IN_SET(r, -ENOENT, -ENOMEDIUM)) + log_debug_errno(r, "Dedicated cgroup not found: %m"); + else + log_warning_errno(r, "Failed to get cgroup: %m"); + } + } + + r = listen_fds(&fd_ctrl, &fd_uevent); + if (r < 0) + return log_error_errno(r, "Failed to listen on fds: %m"); + + r = manager_new(&manager, fd_ctrl, fd_uevent, cgroup); + if (r < 0) + return log_error_errno(r, "Failed to create manager: %m"); + + if (arg_daemonize) { + pid_t pid; + + log_info("Starting version " GIT_VERSION); + + /* connect /dev/null to stdin, stdout, stderr */ + if (log_get_max_level() < LOG_DEBUG) { + r = make_null_stdio(); + if (r < 0) + log_warning_errno(r, "Failed to redirect standard streams to /dev/null: %m"); + } + + pid = fork(); + if (pid < 0) + return log_error_errno(errno, "Failed to fork daemon: %m"); + if (pid > 0) + /* parent */ + return 0; + + /* child */ + (void) setsid(); + } + + return main_loop(manager); +} diff --git a/src/udev/udevd.h b/src/udev/udevd.h new file mode 100644 index 0000000..583e895 --- /dev/null +++ b/src/udev/udevd.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +int run_udevd(int argc, char *argv[]); diff --git a/src/udev/v4l_id/v4l_id.c b/src/udev/v4l_id/v4l_id.c new file mode 100644 index 0000000..932446b --- /dev/null +++ b/src/udev/v4l_id/v4l_id.c @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (c) 2009 Filippo Argiolas <filippo.argiolas@gmail.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details: + */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/time.h> +#include <sys/types.h> +#include <unistd.h> +#include <linux/videodev2.h> + +#include "fd-util.h" +#include "util.h" + +int main(int argc, char *argv[]) { + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + {} + }; + _cleanup_close_ int fd = -1; + char *device; + struct v4l2_capability v2cap; + int c; + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + + switch (c) { + case 'h': + printf("%s [-h,--help] <device file>\n\n" + "Video4Linux device identification.\n\n" + " -h Print this message\n" + , program_invocation_short_name); + return 0; + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + + device = argv[optind]; + if (!device) + return 2; + + fd = open(device, O_RDONLY); + if (fd < 0) + return 3; + + if (ioctl(fd, VIDIOC_QUERYCAP, &v2cap) == 0) { + int capabilities; + printf("ID_V4L_VERSION=2\n"); + printf("ID_V4L_PRODUCT=%s\n", v2cap.card); + printf("ID_V4L_CAPABILITIES=:"); + if (v2cap.capabilities & V4L2_CAP_DEVICE_CAPS) + capabilities = v2cap.device_caps; + else + capabilities = v2cap.capabilities; + if ((capabilities & V4L2_CAP_VIDEO_CAPTURE) > 0 || + (capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) > 0) + printf("capture:"); + if ((capabilities & V4L2_CAP_VIDEO_OUTPUT) > 0 || + (capabilities & V4L2_CAP_VIDEO_OUTPUT_MPLANE) > 0) + printf("video_output:"); + if ((capabilities & V4L2_CAP_VIDEO_OVERLAY) > 0) + printf("video_overlay:"); + if ((capabilities & V4L2_CAP_AUDIO) > 0) + printf("audio:"); + if ((capabilities & V4L2_CAP_TUNER) > 0) + printf("tuner:"); + if ((capabilities & V4L2_CAP_RADIO) > 0) + printf("radio:"); + printf("\n"); + } + + return 0; +} |