diff options
Diffstat (limited to '')
59 files changed, 17926 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..6c2233e --- /dev/null +++ b/src/udev/ata_id/ata_id.c @@ -0,0 +1,651 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 "udev-util.h" +#include "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)) { + 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 (peripheral_device_type != 0x00) { + 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 != NULL) + *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: ata_id [--export] [--help] <device>\n" + " -x,--export print values as environment keys\n" + " -h,--help print this help text\n\n"); + return 0; + } + } + + node = argv[optind]; + if (node == NULL) { + 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..aed24a3 --- /dev/null +++ b/src/udev/cdrom_id/cdrom_id.c @@ -0,0 +1,1048 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 "random-util.h" +#include "udev-util.h" +#include "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 bool is_mounted(const char *device) { + struct stat statbuf; + FILE *fp; + int maj, min; + bool mounted = false; + + if (stat(device, &statbuf) < 0) + return false; + + fp = fopen("/proc/self/mountinfo", "re"); + if (fp == NULL) + return false; + while (fscanf(fp, "%*s %*s %i:%i %*[^\n]", &maj, &min) == 2) { + if (makedev(maj, min) == statbuf.st_rdev) { + mounted = true; + break; + } + } + fclose(fp); + return mounted; +} + +static void info_scsi_cmd_err(const char *cmd, int err) { + if (err == -1) { + log_debug("%s failed", cmd); + return; + } + 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) { + log_debug("CDROM_GET_CAPABILITY failed"); + return -1; + } + + 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) { + log_debug("CDROM_DRIVE_STATUS != CDS_DISC_OK"); + return -1; + } + 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) { + log_debug("not an MMC unit"); + return -1; + } + + 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 { + log_debug("no current profile, assuming no media"); + return -1; + } + }; + + 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 *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 overwite 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) { + log_debug("invalid format capacities length"); + return -1; + } + + 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 + log_debug("format capacities returned no media"); + return -1; + } + } + + /* 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: cdrom_id [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"); + 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|(is_mounted(node) ? 0 : O_EXCL)); + 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 != NULL) + 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/generate-keyboard-keys-gperf.sh b/src/udev/generate-keyboard-keys-gperf.sh new file mode 100755 index 0000000..efb0da2 --- /dev/null +++ b/src/udev/generate-keyboard-keys-gperf.sh @@ -0,0 +1,16 @@ +#!/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..c055f7c --- /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..9d3f6d1 --- /dev/null +++ b/src/udev/meson.build @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +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 +'''.split()) + +systemd_udevd_sources = files('udevd.c') + +libudev_core_sources = ''' + udev.h + udev-ctrl.c + udev-ctrl.h + udev-event.c + udev-node.c + udev-node.h + udev-rules.c + 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 + net/ethtool-util.c + net/ethtool-util.h + net/naming-scheme.c + net/naming-scheme.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', + 'udev.h', + 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'], + ['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'], + link_with : [libudev_static], + install_rpath : udev_rpath, + install : true, + install_dir : udevlibexecdir) +endforeach + +install_data('udev.conf', + install_dir : join_paths(sysconfdir, 'udev')) + +configure_file( + input : 'udev.pc.in', + output : 'udev.pc', + configuration : substs, + install_dir : pkgconfigdatadir == 'no' ? '' : pkgconfigdatadir) + +meson.add_install_script('sh', '-c', + mkdir_p.format(join_paths(sysconfdir, 'udev/rules.d'))) diff --git a/src/udev/mtd_probe/mtd_probe.c b/src/udev/mtd_probe/mtd_probe.c new file mode 100644 index 0000000..f47b133 --- /dev/null +++ b/src/udev/mtd_probe/mtd_probe.c @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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..1d8c93b --- /dev/null +++ b/src/udev/mtd_probe/mtd_probe.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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..f8e1b14 --- /dev/null +++ b/src/udev/mtd_probe/probe_smartmedia.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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/ethtool-util.c b/src/udev/net/ethtool-util.c new file mode 100644 index 0000000..0dcec03 --- /dev/null +++ b/src/udev/net/ethtool-util.c @@ -0,0 +1,787 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include <net/if.h> +#include <sys/ioctl.h> +#include <linux/ethtool.h> +#include <linux/sockios.h> + +#include "conf-parser.h" +#include "ethtool-util.h" +#include "link-config.h" +#include "log.h" +#include "missing.h" +#include "socket-util.h" +#include "string-table.h" +#include "strxcpyx.h" +#include "util.h" + +static const char* const duplex_table[_DUP_MAX] = { + [DUP_FULL] = "full", + [DUP_HALF] = "half" +}; + +DEFINE_STRING_TABLE_LOOKUP(duplex, Duplex); +DEFINE_CONFIG_PARSE_ENUM(config_parse_duplex, duplex, Duplex, "Failed to parse duplex setting"); + +static const char* const wol_table[_WOL_MAX] = { + [WOL_PHY] = "phy", + [WOL_UCAST] = "unicast", + [WOL_MCAST] = "multicast", + [WOL_BCAST] = "broadcast", + [WOL_ARP] = "arp", + [WOL_MAGIC] = "magic", + [WOL_MAGICSECURE] = "secureon", + [WOL_OFF] = "off", +}; + +DEFINE_STRING_TABLE_LOOKUP(wol, WakeOnLan); +DEFINE_CONFIG_PARSE_ENUM(config_parse_wol, wol, WakeOnLan, "Failed to parse WakeOnLan setting"); + +static const char* const port_table[] = { + [NET_DEV_PORT_TP] = "tp", + [NET_DEV_PORT_AUI] = "aui", + [NET_DEV_PORT_MII] = "mii", + [NET_DEV_PORT_FIBRE] = "fibre", + [NET_DEV_PORT_BNC] = "bnc", +}; + +DEFINE_STRING_TABLE_LOOKUP(port, NetDevPort); +DEFINE_CONFIG_PARSE_ENUM(config_parse_port, port, NetDevPort, "Failed to parse Port setting"); + +static const char* const netdev_feature_table[_NET_DEV_FEAT_MAX] = { + [NET_DEV_FEAT_GSO] = "tx-generic-segmentation", + [NET_DEV_FEAT_GRO] = "rx-gro", + [NET_DEV_FEAT_LRO] = "rx-lro", + [NET_DEV_FEAT_TSO] = "tx-tcp-segmentation", + [NET_DEV_FEAT_TSO6] = "tx-tcp6-segmentation", +}; + +static const char* const ethtool_link_mode_bit_table[] = { + [ETHTOOL_LINK_MODE_10baseT_Half_BIT] = "10baset-half", + [ETHTOOL_LINK_MODE_10baseT_Full_BIT] = "10baset-full", + [ETHTOOL_LINK_MODE_100baseT_Half_BIT] = "100baset-half", + [ETHTOOL_LINK_MODE_100baseT_Full_BIT] = "100baset-full", + [ETHTOOL_LINK_MODE_1000baseT_Half_BIT] = "1000baset-half", + [ETHTOOL_LINK_MODE_1000baseT_Full_BIT] = "1000baset-full", + [ETHTOOL_LINK_MODE_Autoneg_BIT] = "autonegotiation", + [ETHTOOL_LINK_MODE_TP_BIT] = "tp", + [ETHTOOL_LINK_MODE_AUI_BIT] = "aui", + [ETHTOOL_LINK_MODE_MII_BIT] = "mii", + [ETHTOOL_LINK_MODE_FIBRE_BIT] = "fibre", + [ETHTOOL_LINK_MODE_BNC_BIT] = "bnc", + [ETHTOOL_LINK_MODE_10000baseT_Full_BIT] = "10000baset-full", + [ETHTOOL_LINK_MODE_Pause_BIT] = "pause", + [ETHTOOL_LINK_MODE_Asym_Pause_BIT] = "asym-pause", + [ETHTOOL_LINK_MODE_2500baseX_Full_BIT] = "2500basex-full", + [ETHTOOL_LINK_MODE_Backplane_BIT] = "backplane", + [ETHTOOL_LINK_MODE_1000baseKX_Full_BIT] = "1000basekx-full", + [ETHTOOL_LINK_MODE_10000baseKX4_Full_BIT] = "10000basekx4-full", + [ETHTOOL_LINK_MODE_10000baseKR_Full_BIT] = "10000basekr-full", + [ETHTOOL_LINK_MODE_10000baseR_FEC_BIT] = "10000baser-fec", + [ETHTOOL_LINK_MODE_20000baseMLD2_Full_BIT] = "20000basemld2-full", + [ETHTOOL_LINK_MODE_20000baseKR2_Full_BIT] = "20000basekr2-full", + [ETHTOOL_LINK_MODE_40000baseKR4_Full_BIT] = "40000basekr4-full", + [ETHTOOL_LINK_MODE_40000baseCR4_Full_BIT] = "40000basecr4-full", + [ETHTOOL_LINK_MODE_40000baseSR4_Full_BIT] = "40000basesr4-full", + [ETHTOOL_LINK_MODE_40000baseLR4_Full_BIT] = "40000baselr4-full", + [ETHTOOL_LINK_MODE_56000baseKR4_Full_BIT] = "56000basekr4-full", + [ETHTOOL_LINK_MODE_56000baseCR4_Full_BIT] = "56000basecr4-full", + [ETHTOOL_LINK_MODE_56000baseSR4_Full_BIT] = "56000basesr4-full", + [ETHTOOL_LINK_MODE_56000baseLR4_Full_BIT] = "56000baselr4-full", + [ETHTOOL_LINK_MODE_25000baseCR_Full_BIT] = "25000basecr-full", + [ETHTOOL_LINK_MODE_25000baseKR_Full_BIT] = "25000basekr-full", + [ETHTOOL_LINK_MODE_25000baseSR_Full_BIT] = "25000basesr-full", + [ETHTOOL_LINK_MODE_50000baseCR2_Full_BIT] = "50000basecr2-full", + [ETHTOOL_LINK_MODE_50000baseKR2_Full_BIT] = "50000basekr2-full", + [ETHTOOL_LINK_MODE_100000baseKR4_Full_BIT] = "100000basekr4-full", + [ETHTOOL_LINK_MODE_100000baseSR4_Full_BIT] = "100000basesr4-full", + [ETHTOOL_LINK_MODE_100000baseCR4_Full_BIT] = "100000basecr4-full", + [ETHTOOL_LINK_MODE_100000baseLR4_ER4_Full_BIT] = "100000baselr4-er4-full", + [ETHTOOL_LINK_MODE_50000baseSR2_Full_BIT] = "50000basesr2-full", + [ETHTOOL_LINK_MODE_1000baseX_Full_BIT] = "1000basex-full", + [ETHTOOL_LINK_MODE_10000baseCR_Full_BIT] = "10000basecr-full", + [ETHTOOL_LINK_MODE_10000baseSR_Full_BIT] = "10000basesr-full", + [ETHTOOL_LINK_MODE_10000baseLR_Full_BIT] = "10000baselr-full", + [ETHTOOL_LINK_MODE_10000baseLRM_Full_BIT] = "10000baselrm-full", + [ETHTOOL_LINK_MODE_10000baseER_Full_BIT] = "10000baseer-full", + [ETHTOOL_LINK_MODE_2500baseT_Full_BIT] = "2500baset-full", + [ETHTOOL_LINK_MODE_5000baseT_Full_BIT] = "5000baset-full", + [ETHTOOL_LINK_MODE_FEC_NONE_BIT] = "fec-none", + [ETHTOOL_LINK_MODE_FEC_RS_BIT] = "fec-rs", + [ETHTOOL_LINK_MODE_FEC_BASER_BIT] = "fec-baser", +}; +/* Make sure the array is large enough to fit all bits */ +assert_cc((ELEMENTSOF(ethtool_link_mode_bit_table)-1) / 32 < ELEMENTSOF(((struct link_config){}).advertise)); + +DEFINE_STRING_TABLE_LOOKUP(ethtool_link_mode_bit, enum ethtool_link_mode_bit_indices); + +int ethtool_connect(int *ret) { + int fd; + + assert_return(ret, -EINVAL); + + fd = socket_ioctl_fd(); + if (fd < 0) + return fd; + + *ret = fd; + + return 0; +} + +int ethtool_get_driver(int *fd, const char *ifname, char **ret) { + struct ethtool_drvinfo ecmd = { + .cmd = ETHTOOL_GDRVINFO + }; + struct ifreq ifr = { + .ifr_data = (void*) &ecmd + }; + char *d; + int r; + + if (*fd < 0) { + r = ethtool_connect(fd); + if (r < 0) + return log_warning_errno(r, "link_config: could not connect to ethtool: %m"); + } + + strscpy(ifr.ifr_name, IFNAMSIZ, ifname); + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + + d = strdup(ecmd.driver); + if (!d) + return -ENOMEM; + + *ret = d; + return 0; +} + +int ethtool_set_speed(int *fd, const char *ifname, unsigned speed, Duplex duplex) { + struct ethtool_cmd ecmd = { + .cmd = ETHTOOL_GSET + }; + struct ifreq ifr = { + .ifr_data = (void*) &ecmd + }; + bool need_update = false; + int r; + + if (speed == 0 && duplex == _DUP_INVALID) + return 0; + + if (*fd < 0) { + r = ethtool_connect(fd); + if (r < 0) + return log_warning_errno(r, "link_config: could not connect to ethtool: %m"); + } + + strscpy(ifr.ifr_name, IFNAMSIZ, ifname); + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + + if (ethtool_cmd_speed(&ecmd) != speed) { + ethtool_cmd_speed_set(&ecmd, speed); + need_update = true; + } + + switch (duplex) { + case DUP_HALF: + if (ecmd.duplex != DUPLEX_HALF) { + ecmd.duplex = DUPLEX_HALF; + need_update = true; + } + break; + case DUP_FULL: + if (ecmd.duplex != DUPLEX_FULL) { + ecmd.duplex = DUPLEX_FULL; + need_update = true; + } + break; + default: + break; + } + + if (need_update) { + ecmd.cmd = ETHTOOL_SSET; + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + } + + return 0; +} + +int ethtool_set_wol(int *fd, const char *ifname, WakeOnLan wol) { + struct ethtool_wolinfo ecmd = { + .cmd = ETHTOOL_GWOL + }; + struct ifreq ifr = { + .ifr_data = (void*) &ecmd + }; + bool need_update = false; + int r; + + if (wol == _WOL_INVALID) + return 0; + + if (*fd < 0) { + r = ethtool_connect(fd); + if (r < 0) + return log_warning_errno(r, "link_config: could not connect to ethtool: %m"); + } + + strscpy(ifr.ifr_name, IFNAMSIZ, ifname); + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + + switch (wol) { + case WOL_PHY: + if (ecmd.wolopts != WAKE_PHY) { + ecmd.wolopts = WAKE_PHY; + need_update = true; + } + break; + case WOL_UCAST: + if (ecmd.wolopts != WAKE_UCAST) { + ecmd.wolopts = WAKE_UCAST; + need_update = true; + } + break; + case WOL_MCAST: + if (ecmd.wolopts != WAKE_MCAST) { + ecmd.wolopts = WAKE_MCAST; + need_update = true; + } + break; + case WOL_BCAST: + if (ecmd.wolopts != WAKE_BCAST) { + ecmd.wolopts = WAKE_BCAST; + need_update = true; + } + break; + case WOL_ARP: + if (ecmd.wolopts != WAKE_ARP) { + ecmd.wolopts = WAKE_ARP; + need_update = true; + } + break; + case WOL_MAGIC: + if (ecmd.wolopts != WAKE_MAGIC) { + ecmd.wolopts = WAKE_MAGIC; + need_update = true; + } + break; + case WOL_MAGICSECURE: + if (ecmd.wolopts != WAKE_MAGICSECURE) { + ecmd.wolopts = WAKE_MAGICSECURE; + need_update = true; + } + break; + case WOL_OFF: + if (ecmd.wolopts != 0) { + ecmd.wolopts = 0; + need_update = true; + } + break; + default: + break; + } + + if (need_update) { + ecmd.cmd = ETHTOOL_SWOL; + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + } + + return 0; +} + +static int get_stringset(int fd, struct ifreq *ifr, int stringset_id, struct ethtool_gstrings **gstrings) { + _cleanup_free_ struct ethtool_gstrings *strings = NULL; + struct { + struct ethtool_sset_info info; + uint32_t space; + } buffer = { + .info = { + .cmd = ETHTOOL_GSSET_INFO, + .sset_mask = UINT64_C(1) << stringset_id, + }, + }; + unsigned len; + int r; + + ifr->ifr_data = (void *) &buffer.info; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + if (!buffer.info.sset_mask) + return -EINVAL; + + len = buffer.info.data[0]; + + strings = malloc0(sizeof(struct ethtool_gstrings) + len * ETH_GSTRING_LEN); + if (!strings) + return -ENOMEM; + + strings->cmd = ETHTOOL_GSTRINGS; + strings->string_set = stringset_id; + strings->len = len; + + ifr->ifr_data = (void *) strings; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + *gstrings = TAKE_PTR(strings); + + return 0; +} + +static int find_feature_index(struct ethtool_gstrings *strings, const char *feature) { + unsigned i; + + for (i = 0; i < strings->len; i++) { + if (streq((char *) &strings->data[i * ETH_GSTRING_LEN], feature)) + return i; + } + + return -1; +} + +int ethtool_set_features(int *fd, const char *ifname, int *features) { + _cleanup_free_ struct ethtool_gstrings *strings = NULL; + struct ethtool_sfeatures *sfeatures; + int block, bit, i, r; + struct ifreq ifr = {}; + + if (*fd < 0) { + r = ethtool_connect(fd); + if (r < 0) + return log_warning_errno(r, "link_config: could not connect to ethtool: %m"); + } + + strscpy(ifr.ifr_name, IFNAMSIZ, ifname); + + r = get_stringset(*fd, &ifr, ETH_SS_FEATURES, &strings); + if (r < 0) + return log_warning_errno(r, "link_config: could not get ethtool features for %s", ifname); + + sfeatures = alloca0(sizeof(struct ethtool_sfeatures) + DIV_ROUND_UP(strings->len, 32U) * sizeof(sfeatures->features[0])); + sfeatures->cmd = ETHTOOL_SFEATURES; + sfeatures->size = DIV_ROUND_UP(strings->len, 32U); + + for (i = 0; i < _NET_DEV_FEAT_MAX; i++) { + + if (features[i] != -1) { + + r = find_feature_index(strings, netdev_feature_table[i]); + if (r < 0) { + log_warning_errno(r, "link_config: could not find feature: %s", netdev_feature_table[i]); + continue; + } + + block = r / 32; + bit = r % 32; + + sfeatures->features[block].valid |= 1 << bit; + + if (features[i]) + sfeatures->features[block].requested |= 1 << bit; + else + sfeatures->features[block].requested &= ~(1 << bit); + } + } + + ifr.ifr_data = (void *) sfeatures; + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return log_warning_errno(r, "link_config: could not set ethtool features for %s", ifname); + + return 0; +} + +static int get_glinksettings(int fd, struct ifreq *ifr, struct ethtool_link_usettings **g) { + struct ecmd { + struct ethtool_link_settings req; + __u32 link_mode_data[3 * ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32]; + } ecmd = { + .req.cmd = ETHTOOL_GLINKSETTINGS, + }; + struct ethtool_link_usettings *u; + unsigned offset; + int r; + + /* The interaction user/kernel via the new API requires a small ETHTOOL_GLINKSETTINGS + handshake first to agree on the length of the link mode bitmaps. If kernel doesn't + agree with user, it returns the bitmap length it is expecting from user as a negative + length (and cmd field is 0). When kernel and user agree, kernel returns valid info in + all fields (ie. link mode length > 0 and cmd is ETHTOOL_GLINKSETTINGS). Based on + https://github.com/torvalds/linux/commit/3f1ac7a700d039c61d8d8b99f28d605d489a60cf + */ + + ifr->ifr_data = (void *) &ecmd; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + if (ecmd.req.link_mode_masks_nwords >= 0 || ecmd.req.cmd != ETHTOOL_GLINKSETTINGS) + return -EOPNOTSUPP; + + ecmd.req.link_mode_masks_nwords = -ecmd.req.link_mode_masks_nwords; + + ifr->ifr_data = (void *) &ecmd; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + if (ecmd.req.link_mode_masks_nwords <= 0 || ecmd.req.cmd != ETHTOOL_GLINKSETTINGS) + return -EOPNOTSUPP; + + u = new0(struct ethtool_link_usettings , 1); + if (!u) + return -ENOMEM; + + u->base = ecmd.req; + + offset = 0; + memcpy(u->link_modes.supported, &ecmd.link_mode_data[offset], 4 * ecmd.req.link_mode_masks_nwords); + + offset += ecmd.req.link_mode_masks_nwords; + memcpy(u->link_modes.advertising, &ecmd.link_mode_data[offset], 4 * ecmd.req.link_mode_masks_nwords); + + offset += ecmd.req.link_mode_masks_nwords; + memcpy(u->link_modes.lp_advertising, &ecmd.link_mode_data[offset], 4 * ecmd.req.link_mode_masks_nwords); + + *g = u; + + return 0; +} + +static int get_gset(int fd, struct ifreq *ifr, struct ethtool_link_usettings **u) { + struct ethtool_link_usettings *e; + struct ethtool_cmd ecmd = { + .cmd = ETHTOOL_GSET, + }; + int r; + + ifr->ifr_data = (void *) &ecmd; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + e = new0(struct ethtool_link_usettings, 1); + if (!e) + return -ENOMEM; + + e->base.cmd = ETHTOOL_GSET; + + e->base.link_mode_masks_nwords = 1; + e->base.speed = ethtool_cmd_speed(&ecmd); + e->base.duplex = ecmd.duplex; + e->base.port = ecmd.port; + e->base.phy_address = ecmd.phy_address; + e->base.autoneg = ecmd.autoneg; + e->base.mdio_support = ecmd.mdio_support; + + e->link_modes.supported[0] = ecmd.supported; + e->link_modes.advertising[0] = ecmd.advertising; + e->link_modes.lp_advertising[0] = ecmd.lp_advertising; + + *u = e; + + return 0; +} + +static int set_slinksettings(int fd, struct ifreq *ifr, const struct ethtool_link_usettings *u) { + struct { + struct ethtool_link_settings req; + __u32 link_mode_data[3 * ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32]; + } ecmd = {}; + unsigned offset; + int r; + + if (u->base.cmd != ETHTOOL_GLINKSETTINGS || u->base.link_mode_masks_nwords <= 0) + return -EINVAL; + + ecmd.req = u->base; + ecmd.req.cmd = ETHTOOL_SLINKSETTINGS; + offset = 0; + memcpy(&ecmd.link_mode_data[offset], u->link_modes.supported, 4 * ecmd.req.link_mode_masks_nwords); + + offset += ecmd.req.link_mode_masks_nwords; + memcpy(&ecmd.link_mode_data[offset], u->link_modes.advertising, 4 * ecmd.req.link_mode_masks_nwords); + + offset += ecmd.req.link_mode_masks_nwords; + memcpy(&ecmd.link_mode_data[offset], u->link_modes.lp_advertising, 4 * ecmd.req.link_mode_masks_nwords); + + ifr->ifr_data = (void *) &ecmd; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + return 0; +} + +static int set_sset(int fd, struct ifreq *ifr, const struct ethtool_link_usettings *u) { + struct ethtool_cmd ecmd = { + .cmd = ETHTOOL_SSET, + }; + int r; + + if (u->base.cmd != ETHTOOL_GSET || u->base.link_mode_masks_nwords <= 0) + return -EINVAL; + + ecmd.supported = u->link_modes.supported[0]; + ecmd.advertising = u->link_modes.advertising[0]; + ecmd.lp_advertising = u->link_modes.lp_advertising[0]; + + ethtool_cmd_speed_set(&ecmd, u->base.speed); + + ecmd.duplex = u->base.duplex; + ecmd.port = u->base.port; + ecmd.phy_address = u->base.phy_address; + ecmd.autoneg = u->base.autoneg; + ecmd.mdio_support = u->base.mdio_support; + ecmd.eth_tp_mdix = u->base.eth_tp_mdix; + ecmd.eth_tp_mdix_ctrl = u->base.eth_tp_mdix_ctrl; + + ifr->ifr_data = (void *) &ecmd; + + r = ioctl(fd, SIOCETHTOOL, ifr); + if (r < 0) + return -errno; + + return 0; +} + +/* If autonegotiation is disabled, the speed and duplex represent the fixed link + * mode and are writable if the driver supports multiple link modes. If it is + * enabled then they are read-only. If the link is up they represent the negotiated + * link mode; if the link is down, the speed is 0, %SPEED_UNKNOWN or the highest + * enabled speed and @duplex is %DUPLEX_UNKNOWN or the best enabled duplex mode. + */ +int ethtool_set_glinksettings(int *fd, const char *ifname, struct link_config *link) { + _cleanup_free_ struct ethtool_link_usettings *u = NULL; + struct ifreq ifr = {}; + int r; + + if (link->autonegotiation != AUTONEG_DISABLE && eqzero(link->advertise)) { + log_info("link_config: autonegotiation is unset or enabled, the speed and duplex are not writable."); + return 0; + } + + if (*fd < 0) { + r = ethtool_connect(fd); + if (r < 0) + return log_warning_errno(r, "link_config: could not connect to ethtool: %m"); + } + + strscpy(ifr.ifr_name, IFNAMSIZ, ifname); + + r = get_glinksettings(*fd, &ifr, &u); + if (r < 0) { + r = get_gset(*fd, &ifr, &u); + if (r < 0) + return log_warning_errno(r, "link_config: Cannot get device settings for %s : %m", ifname); + } + + if (link->speed) + u->base.speed = DIV_ROUND_UP(link->speed, 1000000); + + if (link->duplex != _DUP_INVALID) + u->base.duplex = link->duplex; + + if (link->port != _NET_DEV_PORT_INVALID) + u->base.port = link->port; + + if (link->autonegotiation >= 0) + u->base.autoneg = link->autonegotiation; + + if (!eqzero(link->advertise)) { + u->base.autoneg = AUTONEG_ENABLE; + memcpy(&u->link_modes.advertising, link->advertise, sizeof(link->advertise)); + memzero((uint8_t*) &u->link_modes.advertising + sizeof(link->advertise), + ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NBYTES - sizeof(link->advertise)); + } + + if (u->base.cmd == ETHTOOL_GLINKSETTINGS) + r = set_slinksettings(*fd, &ifr, u); + else + r = set_sset(*fd, &ifr, u); + if (r < 0) + return log_warning_errno(r, "link_config: Cannot set device settings for %s : %m", ifname); + + return r; +} + +int config_parse_channel(const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + link_config *config = data; + uint32_t k; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + r = safe_atou32(rvalue, &k); + if (r < 0) { + log_syntax(unit, LOG_ERR, filename, line, r, "Failed to parse channel value, ignoring: %s", rvalue); + return 0; + } + + if (k < 1) { + log_syntax(unit, LOG_ERR, filename, line, -EINVAL, "Invalid %s value, ignoring: %s", lvalue, rvalue); + return 0; + } + + if (streq(lvalue, "RxChannels")) { + config->channels.rx_count = k; + config->channels.rx_count_set = true; + } else if (streq(lvalue, "TxChannels")) { + config->channels.tx_count = k; + config->channels.tx_count_set = true; + } else if (streq(lvalue, "OtherChannels")) { + config->channels.other_count = k; + config->channels.other_count_set = true; + } else if (streq(lvalue, "CombinedChannels")) { + config->channels.combined_count = k; + config->channels.combined_count_set = true; + } + + return 0; +} + +int ethtool_set_channels(int *fd, const char *ifname, netdev_channels *channels) { + struct ethtool_channels ecmd = { + .cmd = ETHTOOL_GCHANNELS + }; + struct ifreq ifr = { + .ifr_data = (void*) &ecmd + }; + + bool need_update = false; + int r; + + if (*fd < 0) { + r = ethtool_connect(fd); + if (r < 0) + return log_warning_errno(r, "link_config: could not connect to ethtool: %m"); + } + + strscpy(ifr.ifr_name, IFNAMSIZ, ifname); + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + + if (channels->rx_count_set && ecmd.rx_count != channels->rx_count) { + ecmd.rx_count = channels->rx_count; + need_update = true; + } + + if (channels->tx_count_set && ecmd.tx_count != channels->tx_count) { + ecmd.tx_count = channels->tx_count; + need_update = true; + } + + if (channels->other_count_set && ecmd.other_count != channels->other_count) { + ecmd.other_count = channels->other_count; + need_update = true; + } + + if (channels->combined_count_set && ecmd.combined_count != channels->combined_count) { + ecmd.combined_count = channels->combined_count; + need_update = true; + } + + if (need_update) { + ecmd.cmd = ETHTOOL_SCHANNELS; + + r = ioctl(*fd, SIOCETHTOOL, &ifr); + if (r < 0) + return -errno; + } + + return 0; +} + +int config_parse_advertise(const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + link_config *config = data; + const char *p; + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + /* Empty string resets the value. */ + zero(config->advertise); + return 0; + } + + for (p = rvalue;;) { + _cleanup_free_ char *w = NULL; + enum ethtool_link_mode_bit_indices mode; + + r = extract_first_word(&p, &w, NULL, 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_ERR, filename, line, r, "Failed to split advertise modes '%s', ignoring: %m", rvalue); + break; + } + if (r == 0) + break; + + mode = ethtool_link_mode_bit_from_string(w); + if (mode < 0) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Failed to parse advertise mode, ignoring: %s", w); + continue; + } + + config->advertise[mode / 32] |= 1UL << (mode % 32); + } + + return 0; +} diff --git a/src/udev/net/ethtool-util.h b/src/udev/net/ethtool-util.h new file mode 100644 index 0000000..618b26b --- /dev/null +++ b/src/udev/net/ethtool-util.h @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include <macro.h> +#include <linux/ethtool.h> + +#include "conf-parser.h" +#include "missing_network.h" + +struct link_config; + +/* we can't use DUPLEX_ prefix, as it + * clashes with <linux/ethtool.h> */ +typedef enum Duplex { + DUP_HALF = DUPLEX_HALF, + DUP_FULL = DUPLEX_FULL, + _DUP_MAX, + _DUP_INVALID = -1 +} Duplex; + +typedef enum WakeOnLan { + WOL_PHY, + WOL_UCAST, + WOL_MCAST, + WOL_BCAST, + WOL_ARP, + WOL_MAGIC, + WOL_MAGICSECURE, + WOL_OFF, + _WOL_MAX, + _WOL_INVALID = -1 +} WakeOnLan; + +typedef enum NetDevFeature { + NET_DEV_FEAT_GSO, + NET_DEV_FEAT_GRO, + NET_DEV_FEAT_LRO, + NET_DEV_FEAT_TSO, + NET_DEV_FEAT_TSO6, + _NET_DEV_FEAT_MAX, + _NET_DEV_FEAT_INVALID = -1 +} NetDevFeature; + +typedef enum NetDevPort { + NET_DEV_PORT_TP = PORT_TP, + NET_DEV_PORT_AUI = PORT_AUI, + NET_DEV_PORT_MII = PORT_MII, + NET_DEV_PORT_FIBRE = PORT_FIBRE, + NET_DEV_PORT_BNC = PORT_BNC, + NET_DEV_PORT_DA = PORT_DA, + NET_DEV_PORT_NONE = PORT_NONE, + NET_DEV_PORT_OTHER = PORT_OTHER, + _NET_DEV_PORT_MAX, + _NET_DEV_PORT_INVALID = -1 +} NetDevPort; + +#define ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32 (SCHAR_MAX) +#define ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NBYTES (4 * ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32) + +/* layout of the struct passed from/to userland */ +struct ethtool_link_usettings { + struct ethtool_link_settings base; + + struct { + uint32_t supported[ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32]; + uint32_t advertising[ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32]; + uint32_t lp_advertising[ETHTOOL_LINK_MODE_MASK_MAX_KERNEL_NU32]; + } link_modes; +}; + +typedef struct netdev_channels { + uint32_t rx_count; + uint32_t tx_count; + uint32_t other_count; + uint32_t combined_count; + + bool rx_count_set; + bool tx_count_set; + bool other_count_set; + bool combined_count_set; +} netdev_channels; + +int ethtool_connect(int *ret); + +int ethtool_get_driver(int *fd, const char *ifname, char **ret); +int ethtool_set_speed(int *fd, const char *ifname, unsigned speed, Duplex duplex); +int ethtool_set_wol(int *fd, const char *ifname, WakeOnLan wol); +int ethtool_set_features(int *fd, const char *ifname, int *features); +int ethtool_set_glinksettings(int *fd, const char *ifname, struct link_config *link); +int ethtool_set_channels(int *fd, const char *ifname, netdev_channels *channels); + +const char *duplex_to_string(Duplex d) _const_; +Duplex duplex_from_string(const char *d) _pure_; + +const char *wol_to_string(WakeOnLan wol) _const_; +WakeOnLan wol_from_string(const char *wol) _pure_; + +const char *port_to_string(NetDevPort port) _const_; +NetDevPort port_from_string(const char *port) _pure_; + +const char *ethtool_link_mode_bit_to_string(enum ethtool_link_mode_bit_indices val) _const_; +enum ethtool_link_mode_bit_indices ethtool_link_mode_bit_from_string(const char *str) _pure_; + +CONFIG_PARSER_PROTOTYPE(config_parse_duplex); +CONFIG_PARSER_PROTOTYPE(config_parse_wol); +CONFIG_PARSER_PROTOTYPE(config_parse_port); +CONFIG_PARSER_PROTOTYPE(config_parse_channel); +CONFIG_PARSER_PROTOTYPE(config_parse_advertise); diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf new file mode 100644 index 0000000..2bc18bf --- /dev/null +++ b/src/udev/net/link-config-gperf.gperf @@ -0,0 +1,54 @@ +%{ +#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" +%} +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.OriginalName, config_parse_ifnames, 0, offsetof(link_config, match_name) +Match.Path, config_parse_strv, 0, offsetof(link_config, match_path) +Match.Driver, config_parse_strv, 0, offsetof(link_config, match_driver) +Match.Type, config_parse_strv, 0, offsetof(link_config, match_type) +Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(link_config, match_host) +Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(link_config, match_virt) +Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(link_config, match_kernel_cmdline) +Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(link_config, match_kernel_version) +Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(link_config, match_arch) +Link.Description, config_parse_string, 0, offsetof(link_config, description) +Link.MACAddressPolicy, config_parse_mac_policy, 0, offsetof(link_config, mac_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.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_size, 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.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, 0 +Link.TxChannels, config_parse_channel, 0, 0 +Link.OtherChannels, config_parse_channel, 0, 0 +Link.CombinedChannels, config_parse_channel, 0, 0 +Link.Advertise, config_parse_advertise, 0, 0 diff --git a/src/udev/net/link-config.c b/src/udev/net/link-config.c new file mode 100644 index 0000000..eb2477c --- /dev/null +++ b/src/udev/net/link-config.c @@ -0,0 +1,535 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include <netinet/ether.h> + +#include "sd-device.h" +#include "sd-netlink.h" + +#include "alloc-util.h" +#include "conf-files.h" +#include "conf-parser.h" +#include "device-util.h" +#include "ethtool-util.h" +#include "fd-util.h" +#include "link-config.h" +#include "log.h" +#include "missing_network.h" +#include "naming-scheme.h" +#include "netlink-util.h" +#include "network-internal.h" +#include "parse-util.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "random-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "util.h" + +struct link_config_ctx { + LIST_HEAD(link_config, links); + + int ethtool_fd; + + bool enable_name_policy; + + sd_netlink *rtnl; + + usec_t link_dirs_ts_usec; +}; + +static const char* const link_dirs[] = { + "/etc/systemd/network", + "/run/systemd/network", + "/usr/lib/systemd/network", +#if HAVE_SPLIT_USR + "/lib/systemd/network", +#endif + NULL}; + +static void link_config_free(link_config *link) { + if (!link) + return; + + free(link->filename); + + set_free_free(link->match_mac); + strv_free(link->match_path); + strv_free(link->match_driver); + strv_free(link->match_type); + free(link->match_name); + free(link->match_host); + free(link->match_virt); + free(link->match_kernel_cmdline); + free(link->match_kernel_version); + free(link->match_arch); + + free(link->description); + free(link->mac); + free(link->name_policy); + free(link->name); + 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; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(link_config_ctx*, link_config_ctx_free); + +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; +} + +static int load_link(link_config_ctx *ctx, const char *filename) { + _cleanup_(link_config_freep) link_config *link = NULL; + _cleanup_fclose_ FILE *file = NULL; + int i; + int r; + + assert(ctx); + assert(filename); + + file = fopen(filename, "re"); + if (!file) { + if (errno == ENOENT) + return 0; + else + return -errno; + } + + if (null_or_empty_fd(fileno(file))) { + log_debug("Skipping empty file: %s", filename); + return 0; + } + + link = new0(link_config, 1); + if (!link) + return log_oom(); + + link->mac_policy = _MACPOLICY_INVALID; + link->wol = _WOL_INVALID; + link->duplex = _DUP_INVALID; + link->port = _NET_DEV_PORT_INVALID; + link->autonegotiation = -1; + + for (i = 0; i < (int)ELEMENTSOF(link->features); i++) + link->features[i] = -1; + + r = config_parse(NULL, filename, file, + "Match\0Link\0Ethernet\0", + config_item_perf_lookup, link_config_gperf_lookup, + CONFIG_PARSE_WARN, link); + if (r < 0) + return r; + else + log_debug("Parsed configuration file %s", filename); + + if (link->speed > UINT_MAX) + return -ERANGE; + + link->filename = strdup(filename); + if (!link->filename) + return log_oom(); + + LIST_PREPEND(links, ctx->links, link); + link = NULL; + + return 0; +} + +static bool enable_name_policy(void) { + bool b; + + return proc_cmdline_get_bool("net.ifnames", &b) <= 0 || b; +} + +static int link_name_type(sd_device *device, unsigned *type) { + const char *s; + int r; + + r = sd_device_get_sysattr_value(device, "name_assign_type", &s); + if (r < 0) + return log_device_debug_errno(device, r, "Failed to query name_assign_type: %m"); + + r = safe_atou(s, type); + if (r < 0) + return log_device_warning_errno(device, r, "Failed to parse name_assign_type \"%s\": %m", s); + + log_device_debug(device, "Device has name_assign_type=%d", *type); + return 0; +} + +int link_config_load(link_config_ctx *ctx) { + _cleanup_strv_free_ char **files; + 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(link_dirs, &ctx->link_dirs_ts_usec, true); + + r = conf_files_list_strv(&files, ".link", NULL, 0, link_dirs); + if (r < 0) + return log_error_errno(r, "failed to enumerate link files: %m"); + + STRV_FOREACH_BACKWARDS(f, files) { + r = load_link(ctx, *f); + if (r < 0) + return r; + } + + return 0; +} + +bool link_config_should_reload(link_config_ctx *ctx) { + return paths_check_timestamp(link_dirs, &ctx->link_dirs_ts_usec, false); +} + +int link_config_get(link_config_ctx *ctx, sd_device *device, link_config **ret) { + link_config *link; + + assert(ctx); + assert(device); + assert(ret); + + LIST_FOREACH(links, link, ctx->links) { + const char *address = NULL, *id_path = NULL, *parent_driver = NULL, *id_net_driver = NULL, *devtype = NULL, *sysname = NULL; + sd_device *parent; + + (void) sd_device_get_sysattr_value(device, "address", &address); + (void) sd_device_get_property_value(device, "ID_PATH", &id_path); + if (sd_device_get_parent(device, &parent) >= 0) + (void) sd_device_get_driver(parent, &parent_driver); + (void) sd_device_get_property_value(device, "ID_NET_DRIVER", &id_net_driver); + (void) sd_device_get_devtype(device, &devtype); + (void) sd_device_get_sysname(device, &sysname); + + if (net_match_config(link->match_mac, link->match_path, link->match_driver, + link->match_type, link->match_name, link->match_host, + link->match_virt, link->match_kernel_cmdline, + link->match_kernel_version, link->match_arch, + address ? ether_aton(address) : NULL, + id_path, + parent_driver, + id_net_driver, + devtype, + sysname)) { + if (link->match_name) { + unsigned char name_assign_type = NET_NAME_UNKNOWN; + const char *attr_value; + + if (sd_device_get_sysattr_value(device, "name_assign_type", &attr_value) >= 0) + (void) safe_atou8(attr_value, &name_assign_type); + + if (name_assign_type == NET_NAME_ENUM) { + log_warning("Config file %s applies to device based on potentially unpredictable interface name '%s'", + link->filename, sysname); + *ret = link; + + return 0; + } else if (name_assign_type == NET_NAME_RENAMED) { + log_warning("Config file %s matches device based on renamed interface name '%s', ignoring", + link->filename, sysname); + + continue; + } + } + + log_debug("Config file %s applies to device %s", + link->filename, sysname); + + *ret = link; + return 0; + } + } + + *ret = NULL; + return -ENOENT; +} + +static bool mac_is_random(sd_device *device) { + const char *s; + unsigned type; + int r; + + /* if we can't get the assign type, assume it is not random */ + if (sd_device_get_sysattr_value(device, "addr_assign_type", &s) < 0) + return false; + + r = safe_atou(s, &type); + if (r < 0) + return false; + + return type == NET_ADDR_RANDOM; +} + +static int get_mac(sd_device *device, bool want_random, + struct ether_addr *mac) { + int r; + + if (want_random) + random_bytes(mac->ether_addr_octet, ETH_ALEN); + else { + uint64_t result; + + r = net_get_unique_predictable_data(device, &result); + if (r < 0) + return r; + + 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 0; +} + +int link_config_apply(link_config_ctx *ctx, link_config *config, + sd_device *device, const char **name) { + struct ether_addr generated_mac; + struct ether_addr *mac = NULL; + const char *new_name = NULL; + const char *old_name; + unsigned speed, name_type = NET_NAME_UNKNOWN; + NamePolicy policy; + int r, ifindex; + + assert(ctx); + assert(config); + assert(device); + assert(name); + + r = sd_device_get_sysname(device, &old_name); + if (r < 0) + return r; + + r = ethtool_set_glinksettings(&ctx->ethtool_fd, old_name, config); + if (r < 0) { + + if (config->port != _NET_DEV_PORT_INVALID) + log_warning_errno(r, "Could not set port (%s) of %s: %m", port_to_string(config->port), old_name); + + if (!eqzero(config->advertise)) + log_warning_errno(r, "Could not set advertise mode: %m"); /* TODO: include modes in the log message. */ + + if (config->speed) { + speed = DIV_ROUND_UP(config->speed, 1000000); + if (r == -EOPNOTSUPP) { + r = ethtool_set_speed(&ctx->ethtool_fd, old_name, speed, config->duplex); + if (r < 0) + log_warning_errno(r, "Could not set speed of %s to %u Mbps: %m", old_name, speed); + } + } + + if (config->duplex !=_DUP_INVALID) + log_warning_errno(r, "Could not set duplex of %s to (%s): %m", old_name, duplex_to_string(config->duplex)); + } + + r = ethtool_set_wol(&ctx->ethtool_fd, old_name, config->wol); + if (r < 0) + log_warning_errno(r, "Could not set WakeOnLan of %s to %s: %m", + old_name, wol_to_string(config->wol)); + + r = ethtool_set_features(&ctx->ethtool_fd, old_name, config->features); + if (r < 0) + log_warning_errno(r, "Could not set offload features of %s: %m", old_name); + + 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(&ctx->ethtool_fd, old_name, &config->channels); + if (r < 0) + log_warning_errno(r, "Could not set channels of %s: %m", old_name); + } + + r = sd_device_get_ifindex(device, &ifindex); + if (r < 0) + return log_device_warning_errno(device, r, "Could not find ifindex: %m"); + + + (void) link_name_type(device, &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; !new_name && *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 (new_name) + log_device_debug(device, "Policy *%s* yields \"%s\".", name_policy_to_string(policy), new_name); + else if (config->name) { + new_name = config->name; + log_device_debug(device, "Policies didn't yield a name, using specified Name=%s.", new_name); + } else + log_device_debug(device, "Policies didn't yield a name and Name= is not given, not renaming."); + no_rename: + + switch (config->mac_policy) { + case MACPOLICY_PERSISTENT: + if (mac_is_random(device)) { + r = get_mac(device, false, &generated_mac); + if (r == -ENOENT) { + log_warning_errno(r, "Could not generate persistent MAC address for %s: %m", old_name); + break; + } else if (r < 0) + return r; + mac = &generated_mac; + } + break; + case MACPOLICY_RANDOM: + if (!mac_is_random(device)) { + r = get_mac(device, true, &generated_mac); + if (r == -ENOENT) { + log_warning_errno(r, "Could not generate random MAC address for %s: %m", old_name); + break; + } else if (r < 0) + return r; + mac = &generated_mac; + } + break; + case MACPOLICY_NONE: + default: + mac = config->mac; + } + + r = rtnl_set_link_properties(&ctx->rtnl, ifindex, config->alias, mac, config->mtu); + if (r < 0) + return log_warning_errno(r, "Could not set Alias=, MACAddress= or MTU= on %s: %m", old_name); + + *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_policy_table[_MACPOLICY_MAX] = { + [MACPOLICY_PERSISTENT] = "persistent", + [MACPOLICY_RANDOM] = "random", + [MACPOLICY_NONE] = "none", +}; + +DEFINE_STRING_TABLE_LOOKUP(mac_policy, MACPolicy); +DEFINE_CONFIG_PARSE_ENUM(config_parse_mac_policy, mac_policy, MACPolicy, + "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"); diff --git a/src/udev/net/link-config.h b/src/udev/net/link-config.h new file mode 100644 index 0000000..1113b10 --- /dev/null +++ b/src/udev/net/link-config.h @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#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 MACPolicy { + MACPOLICY_PERSISTENT, + MACPOLICY_RANDOM, + MACPOLICY_NONE, + _MACPOLICY_MAX, + _MACPOLICY_INVALID = -1 +} MACPolicy; + +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; + char **match_path; + char **match_driver; + char **match_type; + char **match_name; + Condition *match_host; + Condition *match_virt; + Condition *match_kernel_cmdline; + Condition *match_kernel_version; + Condition *match_arch; + + char *description; + struct ether_addr *mac; + MACPolicy mac_policy; + NamePolicy *name_policy; + char *name; + char *alias; + uint32_t mtu; + size_t speed; + Duplex duplex; + int autonegotiation; + uint32_t advertise[2]; + WakeOnLan wol; + NetDevPort port; + int features[_NET_DEV_FEAT_MAX]; + netdev_channels channels; + + LIST_FIELDS(link_config, links); +}; + +int link_config_ctx_new(link_config_ctx **ret); +void link_config_ctx_free(link_config_ctx *ctx); + +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, struct link_config **ret); +int link_config_apply(link_config_ctx *ctx, struct link_config *config, sd_device *device, const char **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 *mac_policy_to_string(MACPolicy p) _const_; +MACPolicy mac_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_policy); +CONFIG_PARSER_PROTOTYPE(config_parse_name_policy); diff --git a/src/udev/net/naming-scheme.c b/src/udev/net/naming-scheme.c new file mode 100644 index 0000000..27cede5 --- /dev/null +++ b/src/udev/net/naming-scheme.c @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#include "alloc-util.h" +#include "naming-scheme.h" +#include "proc-cmdline.h" +#include "string-util.h" + +static const NamingScheme naming_schemes[] = { + { "v238", NAMING_V238 }, + { "v239", NAMING_V239 }, + { "v240", NAMING_V240 }, + /* … add more schemes here, as the logic to name devices is updated … */ +}; + +static const NamingScheme* naming_scheme_from_name(const char *name) { + size_t i; + + if (streq(name, "latest")) + return naming_schemes + ELEMENTSOF(naming_schemes) - 1; + + for (i = 0; i < ELEMENTSOF(naming_schemes); i++) + if (streq(naming_schemes[i].name, name)) + return naming_schemes + i; + + return NULL; +} + +const NamingScheme* naming_scheme(void) { + static const NamingScheme *cache = NULL; + _cleanup_free_ char *buffer = NULL; + const char *e, *k; + + if (cache) + return cache; + + /* Acquire setting from the kernel command line */ + (void) proc_cmdline_get_key("net.naming-scheme", 0, &buffer); + + /* Also acquire it from an env var */ + e = getenv("NET_NAMING_SCHEME"); + if (e) { + if (*e == ':') { + /* If prefixed with ':' the kernel cmdline takes precedence */ + k = buffer ?: e + 1; + } else + k = e; /* Otherwise the env var takes precedence */ + } else + k = buffer; + + if (k) { + cache = naming_scheme_from_name(k); + if (cache) { + log_info("Using interface naming scheme '%s'.", cache->name); + return cache; + } + + log_warning("Unknown interface naming scheme '%s' requested, ignoring.", k); + } + + cache = naming_scheme_from_name(DEFAULT_NET_NAMING_SCHEME); + assert(cache); + log_info("Using default interface naming scheme '%s'.", cache->name); + + return cache; +} diff --git a/src/udev/net/naming-scheme.h b/src/udev/net/naming-scheme.h new file mode 100644 index 0000000..0b3d9bf --- /dev/null +++ b/src/udev/net/naming-scheme.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include <stdbool.h> + +#include "macro.h" + +/* So here's the deal: net_id is supposed to be an excercise in providing stable names for network devices. However, we + * also want to keep updating the naming scheme used in future versions of net_id. These two goals of course are + * contradictory: on one hand we want things to not change and on the other hand we want them to improve. Our way out + * of this dilemma is to introduce the "naming scheme" concept: each time we improve the naming logic we define a new + * flag for it. Then, we keep a list of schemes, each identified by a name associated with the flags it implements. Via + * a kernel command line and environment variable we then allow the user to pick the scheme they want us to follow: + * installers could "freeze" the used scheme at the moment of installation this way. + * + * Developers: each time you tweak the naming logic here, define a new flag below, and condition the tweak with + * it. Each time we do a release we'll then add a new scheme entry and include all newly defined flags. + * + * Note that this is only half a solution to the problem though: not only udev/net_id gets updated all the time, the + * kernel gets too. And thus a kernel that previously didn't expose some sysfs attribute we look for might eventually + * do, and thus affect our naming scheme too. Thus, enforcing a naming scheme will make interfacing more stable across + * OS versions, but not fully stabilize them. */ +typedef enum NamingSchemeFlags { + /* First, the individual features */ + NAMING_SR_IOV_V = 1 << 0, /* Use "v" suffix for SR-IOV, see 609948c7043a40008b8299529c978ed8e11de8f6*/ + NAMING_NPAR_ARI = 1 << 1, /* Use NPAR "ARI", see 6bc04997b6eab35d1cb9fa73889892702c27be09 */ + NAMING_INFINIBAND = 1 << 2, /* Use "ib" prefix for infiniband, see 938d30aa98df887797c9e05074a562ddacdcdf5e */ + NAMING_ZERO_ACPI_INDEX = 1 << 3, /* Allow zero acpi_index field, see d81186ef4f6a888a70f20a1e73a812d6acb9e22f */ + NAMING_ALLOW_RERENAMES = 1 << 4, /* Allow re-renaming of devices, see #9006 */ + + /* And now the masks that combine the features above */ + NAMING_V238 = 0, + NAMING_V239 = NAMING_V238 | NAMING_SR_IOV_V | NAMING_NPAR_ARI, + NAMING_V240 = NAMING_V239 | NAMING_INFINIBAND | NAMING_ZERO_ACPI_INDEX | NAMING_ALLOW_RERENAMES, + + _NAMING_SCHEME_FLAGS_INVALID = -1, +} NamingSchemeFlags; + +typedef struct NamingScheme { + const char *name; + NamingSchemeFlags flags; +} NamingScheme; + +const NamingScheme* naming_scheme(void); + +static inline bool naming_scheme_has(NamingSchemeFlags flags) { + return FLAGS_SET(naming_scheme()->flags, flags); +} 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..208b3e7 --- /dev/null +++ b/src/udev/scsi_id/scsi.h @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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..2698cdd --- /dev/null +++ b/src/udev/scsi_id/scsi_id.c @@ -0,0 +1,597 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 <string.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 == NULL) { + 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 == NULL) + 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 == NULL) { + if (vendor_in == NULL) + 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 { + log_error("Unknown page code '%s'", optarg); + return -1; + } + break; + + case 's': + sg_version = atoi(optarg); + if (sg_version < 3 || sg_version > 4) { + log_error("Unknown SG version '%s'", optarg); + return -1; + } + 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..70e804f --- /dev/null +++ b/src/udev/scsi_id/scsi_id.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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..c67d047 --- /dev/null +++ b/src/udev/scsi_id/scsi_serial.c @@ -0,0 +1,897 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 <string.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include "random-util.h" +#include "scsi.h" +#include "scsi_id.h" +#include "string-util.h" +#include "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) { + log_debug("%s: sense buffer empty", dev_scsi->kernel); + return -1; + } + + 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) { + log_debug("%s: sense buffer too small %d bytes, %d bytes too short", + dev_scsi->kernel, sb_len, s - sb_len); + return -1; + } + if (IN_SET(code, 0x0, 0x1)) { + sense_key = sense_buffer[2] & 0xf; + if (s < 14) { + /* + * Possible? + */ + log_debug("%s: sense result too" " small %d bytes", + dev_scsi->kernel, s); + return -1; + } + 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 { + log_debug("%s: invalid sense code 0x%x", + dev_scsi->kernel, code); + return -1; + } + 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) { + log_debug("%s: sense buffer too small %d bytes, %d bytes too short", + dev_scsi->kernel, sb_len, 4 - sb_len); + return -1; + } + + 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. + */ + log_debug("%s: called with no error", __FUNCTION__); + return -1; + } + + 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. + */ + log_debug("%s: called with no error", __FUNCTION__); + return -1; + } + + 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) { + log_debug("buflen %d too long", buflen); + return -1; + } + +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; +} + +/* + * The caller checks that serial is long enough to include the vendor + + * model. + */ +static int prepend_vendor_model(struct scsi_id_device *dev_scsi, char *serial) { + int ind; + + strncpy(serial, dev_scsi->vendor, VENDOR_LENGTH); + strncat(serial, dev_scsi->model, MODEL_LENGTH); + ind = strlen(serial); + + /* + * This is not a complete check, since we are using strncat/cpy + * above, ind will never be too large. + */ + if (ind != (VENDOR_LENGTH + MODEL_LENGTH)) { + log_debug("%s: expected length %d, got length %d", + dev_scsi->kernel, (VENDOR_LENGTH + MODEL_LENGTH), ind); + return -1; + } + return ind; +} + +/* + * 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 (prepend_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 != NULL) + 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[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 != NULL) { + serial[0] = 'S'; + ser_ind = prepend_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 != NULL) { + 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..69d6c4b --- /dev/null +++ b/src/udev/udev-builtin-blkid.c @@ -0,0 +1,318 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * probe disks for filesystems and partitions + * + * Copyright © 2011 Karel Zak <kzak@redhat.com> + */ + +#include <blkid.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "blkid-util.h" +#include "device-util.h" +#include "efivars.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 ?: -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); + 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 struct udev_builtin 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..956be59 --- /dev/null +++ b/src/udev/udev-builtin-btrfs.c @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include <fcntl.h> +#include <stdlib.h> +#include <sys/ioctl.h> + +#include "device-util.h" +#include "fd-util.h" +#include "missing.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 struct udev_builtin 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..225e026 --- /dev/null +++ b/src/udev/udev-builtin-hwdb.c @@ -0,0 +1,218 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#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; + 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; + snprintf(s, size, "usb:v%04Xp%04X*", vn, pn); + 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) { + sd_device *d; + char s[16]; + bool last = false; + int r = 0; + + assert(dev); + + if (!srcdev) + srcdev = dev; + + for (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; + + 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 lookup 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 lookup 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 struct udev_builtin 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..e3db55b --- /dev/null +++ b/src/udev/udev-builtin-input_id.c @@ -0,0 +1,364 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * expose input properties via udev + * + * Portions Copyright © 2004 David Zeuthen, <david@fubar.dk> + * Copyright © 2014 Carlos Garnacho <carlosg@gnome.org> + */ + +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <linux/limits.h> +#include <linux/input.h> + +#include "device-util.h" +#include "fd-util.h" +#include "missing.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; + } + } +} + +/* pointer devices */ +static bool test_pointers(sd_device *dev, + 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 is_direct = false; + bool has_touch = false; + bool has_3d_coordinates = false; + bool has_keys = false; + bool stylus_or_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_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); + stylus_or_pen = test_bit(BTN_STYLUS, bitmask_key) || 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); + + /* 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 (stylus_or_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 (stylus_or_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 && !is_touchpad && !is_joystick && + has_mouse_button && + (has_rel_coordinates || + !has_abs_coordinates)) /* mouse buttons and no axis */ + is_mouse = 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"); + + 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) { + /* 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, 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 struct udev_builtin 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..d80cdd2 --- /dev/null +++ b/src/udev/udev-builtin-keyboard.c @@ -0,0 +1,258 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.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 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; + char *next; + int r; + + r = ioctl(fd, EVIOCGABS(evcode), &absinfo); + if (r < 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(dev, "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); + r = ioctl(fd, EVIOCSABS(evcode), &absinfo); + if (r < 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 struct udev_builtin 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..ce52149 --- /dev/null +++ b/src/udev/udev-builtin-kmod.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * load kernel modules + * + * Copyright © 2011 ProFUSION embedded systems + */ + +#include <errno.h> +#include <libkmod.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 struct udev_builtin 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..03b281a --- /dev/null +++ b/src/udev/udev-builtin-net_id.c @@ -0,0 +1,961 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +/* + * 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 + * + * http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames + * + * Two character prefixes based on the type of interface: + * en — Ethernet + * ib — InfiniBand + * sl — serial line IP (slip) + * wl — wlan + * ww — wwan + * + * Type of names: + * b<number> — BCMA bus core number + * c<bus_id> — bus id of a grouped CCW or CCW device, + * with all leading zeros stripped [s390] + * o<index>[n<phys_port_name>|d<dev_port>] + * — on-board device index number + * s<slot>[f<function>][n<phys_port_name>|d<dev_port>] + * — hotplug slot index number + * x<MAC> — MAC address + * [P<domain>]p<bus>s<slot>[f<function>][n<phys_port_name>|d<dev_port>] + * — PCI geographical location + * [P<domain>]p<bus>s<slot>[f<function>][u<port>][..][c<config>][i<interface>] + * — USB port number chain + * v<slot> - VIO slot number (IBM PowerVM) + * a<vendor><model>i<instance> — Platform bus ACPI instance id + * + * All multi-function PCI devices will carry the [f<function>] number in the + * device name, including the function 0 device. + * + * SR-IOV virtual devices are named based on the name of the parent interface, + * with a suffix of "v<N>", where <N> is the virtual device number. + * + * When using PCI geography, The PCI domain is only prepended when it is not 0. + * + * For USB devices the full chain of port numbers of hubs is composed. If the + * name gets longer than the maximum number of 15 characters, the name is not + * exported. + * The usual USB configuration == 1 and interface == 0 values are suppressed. + * + * PCI Ethernet card with firmware index "1": + * ID_NET_NAME_ONBOARD=eno1 + * ID_NET_NAME_ONBOARD_LABEL=Ethernet Port 1 + * + * PCI Ethernet card in hotplug slot with firmware index number: + * /sys/devices/pci0000:00/0000:00:1c.3/0000:05:00.0/net/ens1 + * ID_NET_NAME_MAC=enx000000000466 + * ID_NET_NAME_PATH=enp5s0 + * ID_NET_NAME_SLOT=ens1 + * + * PCI Ethernet multi-function card with 2 ports: + * /sys/devices/pci0000:00/0000:00:1c.0/0000:02:00.0/net/enp2s0f0 + * ID_NET_NAME_MAC=enx78e7d1ea46da + * ID_NET_NAME_PATH=enp2s0f0 + * /sys/devices/pci0000:00/0000:00:1c.0/0000:02:00.1/net/enp2s0f1 + * ID_NET_NAME_MAC=enx78e7d1ea46dc + * ID_NET_NAME_PATH=enp2s0f1 + * + * PCI wlan card: + * /sys/devices/pci0000:00/0000:00:1c.1/0000:03:00.0/net/wlp3s0 + * ID_NET_NAME_MAC=wlx0024d7e31130 + * ID_NET_NAME_PATH=wlp3s0 + * + * PCI IB host adapter with 2 ports: + * /sys/devices/pci0000:00/0000:00:03.0/0000:15:00.0/net/ibp21s0f0 + * ID_NET_NAME_PATH=ibp21s0f0 + * /sys/devices/pci0000:00/0000:00:03.0/0000:15:00.1/net/ibp21s0f1 + * ID_NET_NAME_PATH=ibp21s0f1 + * + * USB built-in 3G modem: + * /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.4/2-1.4:1.6/net/wwp0s29u1u4i6 + * ID_NET_NAME_MAC=wwx028037ec0200 + * ID_NET_NAME_PATH=wwp0s29u1u4i6 + * + * USB Android phone: + * /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/net/enp0s29u1u2 + * ID_NET_NAME_MAC=enxd626b3450fb5 + * ID_NET_NAME_PATH=enp0s29u1u2 + * + * s390 grouped CCW interface: + * /sys/devices/css0/0.0.0007/0.0.f5f0/group_device/net/encf5f0 + * ID_NET_NAME_MAC=enx026d3c00000a + * ID_NET_NAME_PATH=encf5f0 + */ + +#include <errno.h> +#include <fcntl.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.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 "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, +}; + +struct netnames { + enum netname_type type; + + uint8_t mac[6]; + bool mac_valid; + + sd_device *pcidev; + char pci_slot[IFNAMSIZ]; + char pci_path[IFNAMSIZ]; + char pci_onboard[IFNAMSIZ]; + const char *pci_onboard_label; + + char usb_ports[IFNAMSIZ]; + char bcma_core[IFNAMSIZ]; + char ccw_busid[IFNAMSIZ]; + char vio_slot[IFNAMSIZ]; + char platform_path[IFNAMSIZ]; +}; + +struct virtfn_info { + sd_device *physfn_pcidev; + char suffix[IFNAMSIZ]; +}; + +/* 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[IFNAMSIZ]; + 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); + 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 = strjoin(physfn_pci_syspath, "/", dent->d_name); + if (!virtfn_link_file) + return -ENOMEM; + + if (chase_symlinks(virtfn_link_file, NULL, 0, &virtfn_pci_syspath) < 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 his 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 int dev_pci_slot(sd_device *dev, struct netnames *names) { + unsigned long dev_port = 0; + unsigned domain, bus, slot, func, hotplug_slot = 0; + 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) { + unsigned i; + char str[PATH_MAX]; + _cleanup_free_ char *address = NULL; + + if (dent->d_name[0] == '.') + continue; + r = safe_atou_full(dent->d_name, 10, &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; + 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. + */ + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + if (sscanf(syspath, pattern, vendor, &model, &instance, ðid) != 4) + return -EINVAL; +#pragma GCC diagnostic pop + + 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 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; +} + +/* 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[IFNAMSIZ]; + + 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[IFNAMSIZ]; + + 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[IFNAMSIZ]; + + 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[IFNAMSIZ]; + + 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 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[IFNAMSIZ]; + + 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", 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[IFNAMSIZ]; + + 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[IFNAMSIZ]; + + 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 struct udev_builtin 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..a845dfa --- /dev/null +++ b/src/udev/udev-builtin-net_setup_link.c @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#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) + 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 struct udev_builtin 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..7ce1c56 --- /dev/null +++ b/src/udev/udev-builtin-path_id.c @@ -0,0 +1,683 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 <string.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) { + sd_device *targetdev, *target_parent; + _cleanup_(sd_device_unrefp) sd_device *atadev = NULL; + const char *port_no, *sysname; + + assert(parent); + assert(path); + + 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; + + path_prepend(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, 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); + + 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; + 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, &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); + parent = skip_subsystem(parent, "pci"); + supported_parent = true; + } else if (streq(subsys, "platform")) { + path_prepend(&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); + parent = skip_subsystem(parent, "acpi"); + supported_parent = true; + } else if (streq(subsys, "xen")) { + path_prepend(&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); + parent = skip_subsystem(parent, "scm"); + supported_transport = true; + supported_parent = true; + } else if (streq(subsys, "ccw")) { + path_prepend(&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); + 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); + 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); + parent = skip_subsystem(parent, "nvme"); + supported_parent = true; + supported_transport = true; + } + } + + 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); + } + + return 0; +} + +const struct udev_builtin 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..10a143a --- /dev/null +++ b/src/udev/udev-builtin-uaccess.c @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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(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(dev, k == -ENOENT ? LOG_DEBUG : LOG_ERR, k, "Failed to apply ACL: %m"); + if (r >= 0) + r = k; + } + } + + return r; +} + +const struct udev_builtin 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..3525d25 --- /dev/null +++ b/src/udev/udev-builtin-usb_id.c @@ -0,0 +1,460 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 <string.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_free_ char *filename = NULL; + _cleanup_close_ int fd = -1; + ssize_t size; + unsigned char buf[18 + 65535]; + size_t pos = 0; + unsigned strpos = 0; + const char *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; + if (asprintf(&filename, "%s/descriptors", syspath) < 0) + return log_oom(); + + fd = open(filename, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_device_debug_errno(dev, errno, "Failed to open USB device 'descriptors' file: %m"); + + size = read(fd, buf, sizeof(buf)); + if (size < 18 || (size_t) size >= sizeof(buf)) + return -EIO; + + 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; + 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) != NULL) + 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"); + + /* fallback 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 struct udev_builtin 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..48ce295 --- /dev/null +++ b/src/udev/udev-builtin.c @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include <getopt.h> +#include <stdio.h> +#include <string.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 struct udev_builtin *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(enum udev_builtin_cmd cmd) { + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + + if (!builtins[cmd]) + return NULL; + + return builtins[cmd]->name; +} + +bool udev_builtin_run_once(enum udev_builtin_cmd cmd) { + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + + if (!builtins[cmd]) + return false; + + return builtins[cmd]->run_once; +} + +enum udev_builtin_cmd udev_builtin_lookup(const char *command) { + enum udev_builtin_cmd 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, enum udev_builtin_cmd cmd, const char *command, bool test) { + _cleanup_strv_free_ char **argv = NULL; + + assert(dev); + assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX); + assert(command); + + if (!builtins[cmd]) + return -EOPNOTSUPP; + + argv = strv_split_full(command, NULL, SPLIT_QUOTES | SPLIT_RELAX); + if (!argv) + return -ENOMEM; + + /* 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..e51eefb --- /dev/null +++ b/src/udev/udev-builtin.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#pragma once + +#include <stdbool.h> + +#include "sd-device.h" + +enum udev_builtin_cmd { +#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, +}; + +struct udev_builtin { + 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; +}; + +#if HAVE_BLKID +extern const struct udev_builtin udev_builtin_blkid; +#endif +extern const struct udev_builtin udev_builtin_btrfs; +extern const struct udev_builtin udev_builtin_hwdb; +extern const struct udev_builtin udev_builtin_input_id; +extern const struct udev_builtin udev_builtin_keyboard; +#if HAVE_KMOD +extern const struct udev_builtin udev_builtin_kmod; +#endif +extern const struct udev_builtin udev_builtin_net_id; +extern const struct udev_builtin udev_builtin_net_setup_link; +extern const struct udev_builtin udev_builtin_path_id; +extern const struct udev_builtin udev_builtin_usb_id; +#if HAVE_ACL +extern const struct udev_builtin udev_builtin_uaccess; +#endif + +void udev_builtin_init(void); +void udev_builtin_exit(void); +enum udev_builtin_cmd udev_builtin_lookup(const char *command); +const char *udev_builtin_name(enum udev_builtin_cmd cmd); +bool udev_builtin_run_once(enum udev_builtin_cmd cmd); +int udev_builtin_run(sd_device *dev, enum udev_builtin_cmd 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..c217815 --- /dev/null +++ b/src/udev/udev-ctrl.c @@ -0,0 +1,427 @@ +/* SPDX-License-Identifier: LGPL-2.1+ + * + * 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/socket.h> +#include <sys/un.h> +#include <unistd.h> + +#include "alloc-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 + +enum udev_ctrl_msg_type { + UDEV_CTRL_UNKNOWN, + 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, +}; + +struct udev_ctrl_msg_wire { + char version[16]; + unsigned magic; + enum udev_ctrl_msg_type type; + union { + int intval; + char buf[256]; + }; +}; + +struct udev_ctrl_msg { + unsigned n_ref; + struct udev_ctrl_connection *conn; + struct udev_ctrl_msg_wire ctrl_msg_wire; +}; + +struct udev_ctrl { + unsigned n_ref; + int sock; + union sockaddr_union saddr; + socklen_t addrlen; + bool bound; + bool cleanup_socket; + bool connected; +}; + +struct udev_ctrl_connection { + unsigned n_ref; + struct udev_ctrl *uctrl; + int sock; +}; + +struct udev_ctrl *udev_ctrl_new_from_fd(int fd) { + struct udev_ctrl *uctrl; + int r; + + uctrl = new0(struct udev_ctrl, 1); + if (!uctrl) + return NULL; + uctrl->n_ref = 1; + + if (fd < 0) { + uctrl->sock = socket(AF_LOCAL, SOCK_SEQPACKET|SOCK_NONBLOCK|SOCK_CLOEXEC, 0); + if (uctrl->sock < 0) { + log_error_errno(errno, "Failed to create socket: %m"); + udev_ctrl_unref(uctrl); + return NULL; + } + } else { + uctrl->bound = true; + uctrl->sock = fd; + } + + /* + * FIXME: remove it as soon as we can depend on this: + * http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=90c6bd34f884cd9cee21f1d152baf6c18bcac949 + */ + r = setsockopt_int(uctrl->sock, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + log_warning_errno(r, "Failed to set SO_PASSCRED: %m"); + + uctrl->saddr.un = (struct sockaddr_un) { + .sun_family = AF_UNIX, + .sun_path = "/run/udev/control", + }; + + uctrl->addrlen = SOCKADDR_UN_LEN(uctrl->saddr.un); + return uctrl; +} + +struct udev_ctrl *udev_ctrl_new(void) { + return udev_ctrl_new_from_fd(-1); +} + +int udev_ctrl_enable_receiving(struct udev_ctrl *uctrl) { + int err; + + if (!uctrl->bound) { + err = bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen); + if (err < 0 && errno == EADDRINUSE) { + (void) sockaddr_un_unlink(&uctrl->saddr.un); + err = bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen); + } + + if (err < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + err = listen(uctrl->sock, 0); + if (err < 0) + return log_error_errno(errno, "Failed to listen: %m"); + + uctrl->bound = true; + uctrl->cleanup_socket = true; + } + return 0; +} + +static struct udev_ctrl *udev_ctrl_free(struct udev_ctrl *uctrl) { + assert(uctrl); + + safe_close(uctrl->sock); + return mfree(uctrl); +} + +DEFINE_PRIVATE_TRIVIAL_REF_FUNC(struct udev_ctrl, udev_ctrl); +DEFINE_TRIVIAL_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_get_fd(struct udev_ctrl *uctrl) { + if (!uctrl) + return -EINVAL; + return uctrl->sock; +} + +struct udev_ctrl_connection *udev_ctrl_get_connection(struct udev_ctrl *uctrl) { + struct udev_ctrl_connection *conn; + struct ucred ucred = {}; + int r; + + conn = new(struct udev_ctrl_connection, 1); + if (!conn) + return NULL; + conn->n_ref = 1; + conn->uctrl = uctrl; + + conn->sock = accept4(uctrl->sock, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK); + if (conn->sock < 0) { + if (errno != EINTR) + log_error_errno(errno, "Failed to receive ctrl connection: %m"); + goto err; + } + + /* check peer credential of connection */ + r = getpeercred(conn->sock, &ucred); + if (r < 0) { + log_error_errno(r, "Failed to receive credentials of ctrl connection: %m"); + goto err; + } + if (ucred.uid > 0) { + log_error("Sender uid="UID_FMT", message ignored", ucred.uid); + goto err; + } + + /* enable receiving of the sender credentials in the messages */ + r = setsockopt_int(conn->sock, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + log_warning_errno(r, "Failed to set SO_PASSCRED: %m"); + + udev_ctrl_ref(uctrl); + return conn; +err: + safe_close(conn->sock); + return mfree(conn); +} + +static struct udev_ctrl_connection *udev_ctrl_connection_free(struct udev_ctrl_connection *conn) { + assert(conn); + + safe_close(conn->sock); + udev_ctrl_unref(conn->uctrl); + return mfree(conn); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(struct udev_ctrl_connection, udev_ctrl_connection, udev_ctrl_connection_free); + +static int ctrl_send(struct udev_ctrl *uctrl, enum udev_ctrl_msg_type type, int intval, const char *buf, usec_t timeout) { + struct udev_ctrl_msg_wire ctrl_msg_wire = { + .version = "udev-" STRINGIFY(PROJECT_VERSION), + .magic = UDEV_CTRL_MAGIC, + .type = type, + }; + + if (buf) + strscpy(ctrl_msg_wire.buf, sizeof(ctrl_msg_wire.buf), buf); + else + ctrl_msg_wire.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; + + /* wait for peer message handling or disconnect */ + for (;;) { + struct pollfd pfd = { + .fd = uctrl->sock, + .events = POLLIN, + }; + int r; + + r = poll(&pfd, 1, DIV_ROUND_UP(timeout, USEC_PER_MSEC)); + if (r < 0) { + if (errno == EINTR) + continue; + return -errno; + } + if (r == 0) + return -ETIMEDOUT; + if (pfd.revents & POLLERR) + return -EIO; + return 0; + } +} + +int udev_ctrl_send_set_log_level(struct udev_ctrl *uctrl, int priority, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_SET_LOG_LEVEL, priority, NULL, timeout); +} + +int udev_ctrl_send_stop_exec_queue(struct udev_ctrl *uctrl, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_STOP_EXEC_QUEUE, 0, NULL, timeout); +} + +int udev_ctrl_send_start_exec_queue(struct udev_ctrl *uctrl, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_START_EXEC_QUEUE, 0, NULL, timeout); +} + +int udev_ctrl_send_reload(struct udev_ctrl *uctrl, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_RELOAD, 0, NULL, timeout); +} + +int udev_ctrl_send_set_env(struct udev_ctrl *uctrl, const char *key, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_SET_ENV, 0, key, timeout); +} + +int udev_ctrl_send_set_children_max(struct udev_ctrl *uctrl, int count, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_SET_CHILDREN_MAX, count, NULL, timeout); +} + +int udev_ctrl_send_ping(struct udev_ctrl *uctrl, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_PING, 0, NULL, timeout); +} + +int udev_ctrl_send_exit(struct udev_ctrl *uctrl, usec_t timeout) { + return ctrl_send(uctrl, UDEV_CTRL_EXIT, 0, NULL, timeout); +} + +struct udev_ctrl_msg *udev_ctrl_receive_msg(struct udev_ctrl_connection *conn) { + struct udev_ctrl_msg *uctrl_msg; + ssize_t size; + struct cmsghdr *cmsg; + struct iovec iov; + char cred_msg[CMSG_SPACE(sizeof(struct ucred))]; + struct msghdr smsg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cred_msg, + .msg_controllen = sizeof(cred_msg), + }; + struct ucred *cred; + + uctrl_msg = new0(struct udev_ctrl_msg, 1); + if (!uctrl_msg) + return NULL; + uctrl_msg->n_ref = 1; + uctrl_msg->conn = conn; + udev_ctrl_connection_ref(conn); + + /* wait for the incoming message */ + for (;;) { + struct pollfd pfd[1]; + int r; + + pfd[0].fd = conn->sock; + pfd[0].events = POLLIN; + + r = poll(pfd, 1, 10000); + if (r < 0) { + if (errno == EINTR) + continue; + goto err; + } else if (r == 0) { + log_error("Timeout waiting for ctrl message"); + goto err; + } else { + if (!(pfd[0].revents & POLLIN)) { + log_error("Invalid ctrl connection: %m"); + goto err; + } + } + + break; + } + + iov = IOVEC_MAKE(&uctrl_msg->ctrl_msg_wire, sizeof(struct udev_ctrl_msg_wire)); + + size = recvmsg(conn->sock, &smsg, 0); + if (size < 0) { + log_error_errno(errno, "Failed to receive ctrl message: %m"); + goto err; + } + + cmsg_close_all(&smsg); + + cmsg = CMSG_FIRSTHDR(&smsg); + + if (!cmsg || cmsg->cmsg_type != SCM_CREDENTIALS) { + log_error("No sender credentials received, ignoring message"); + goto err; + } + + cred = (struct ucred *) CMSG_DATA(cmsg); + + if (cred->uid != 0) { + log_error("Sender uid="UID_FMT", ignoring message", cred->uid); + goto err; + } + + if (uctrl_msg->ctrl_msg_wire.magic != UDEV_CTRL_MAGIC) { + log_error("Message magic 0x%08x doesn't match, ignoring", uctrl_msg->ctrl_msg_wire.magic); + goto err; + } + + return uctrl_msg; +err: + udev_ctrl_msg_unref(uctrl_msg); + return NULL; +} + +static struct udev_ctrl_msg *udev_ctrl_msg_free(struct udev_ctrl_msg *ctrl_msg) { + assert(ctrl_msg); + + udev_ctrl_connection_unref(ctrl_msg->conn); + return mfree(ctrl_msg); +} + +DEFINE_TRIVIAL_UNREF_FUNC(struct udev_ctrl_msg, udev_ctrl_msg, udev_ctrl_msg_free); + +int udev_ctrl_get_set_log_level(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_SET_LOG_LEVEL) + return ctrl_msg->ctrl_msg_wire.intval; + return -1; +} + +int udev_ctrl_get_stop_exec_queue(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_STOP_EXEC_QUEUE) + return 1; + return -1; +} + +int udev_ctrl_get_start_exec_queue(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_START_EXEC_QUEUE) + return 1; + return -1; +} + +int udev_ctrl_get_reload(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_RELOAD) + return 1; + return -1; +} + +const char *udev_ctrl_get_set_env(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_SET_ENV) + return ctrl_msg->ctrl_msg_wire.buf; + return NULL; +} + +int udev_ctrl_get_set_children_max(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_SET_CHILDREN_MAX) + return ctrl_msg->ctrl_msg_wire.intval; + return -1; +} + +int udev_ctrl_get_ping(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_PING) + return 1; + return -1; +} + +int udev_ctrl_get_exit(struct udev_ctrl_msg *ctrl_msg) { + if (ctrl_msg->ctrl_msg_wire.type == UDEV_CTRL_EXIT) + return 1; + return -1; +} diff --git a/src/udev/udev-ctrl.h b/src/udev/udev-ctrl.h new file mode 100644 index 0000000..8e452a4 --- /dev/null +++ b/src/udev/udev-ctrl.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#pragma once + +#include "macro.h" +#include "time-util.h" + +struct udev_ctrl; +struct udev_ctrl *udev_ctrl_new(void); +struct udev_ctrl *udev_ctrl_new_from_fd(int fd); +int udev_ctrl_enable_receiving(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_get_fd(struct udev_ctrl *uctrl); +int udev_ctrl_send_set_log_level(struct udev_ctrl *uctrl, int priority, usec_t timeout); +int udev_ctrl_send_stop_exec_queue(struct udev_ctrl *uctrl, usec_t timeout); +int udev_ctrl_send_start_exec_queue(struct udev_ctrl *uctrl, usec_t timeout); +int udev_ctrl_send_reload(struct udev_ctrl *uctrl, usec_t timeout); +int udev_ctrl_send_ping(struct udev_ctrl *uctrl, usec_t timeout); +int udev_ctrl_send_exit(struct udev_ctrl *uctrl, usec_t timeout); +int udev_ctrl_send_set_env(struct udev_ctrl *uctrl, const char *key, usec_t timeout); +int udev_ctrl_send_set_children_max(struct udev_ctrl *uctrl, int count, usec_t timeout); + +struct udev_ctrl_connection; +struct udev_ctrl_connection *udev_ctrl_get_connection(struct udev_ctrl *uctrl); +struct udev_ctrl_connection *udev_ctrl_connection_ref(struct udev_ctrl_connection *conn); +struct udev_ctrl_connection *udev_ctrl_connection_unref(struct udev_ctrl_connection *conn); + +struct udev_ctrl_msg; +struct udev_ctrl_msg *udev_ctrl_receive_msg(struct udev_ctrl_connection *conn); +struct udev_ctrl_msg *udev_ctrl_msg_unref(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_set_log_level(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_stop_exec_queue(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_start_exec_queue(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_reload(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_ping(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_exit(struct udev_ctrl_msg *ctrl_msg); +const char *udev_ctrl_get_set_env(struct udev_ctrl_msg *ctrl_msg); +int udev_ctrl_get_set_children_max(struct udev_ctrl_msg *ctrl_msg); + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl*, udev_ctrl_unref); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl_connection*, udev_ctrl_connection_unref); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_ctrl_msg*, udev_ctrl_msg_unref); diff --git a/src/udev/udev-event.c b/src/udev/udev-event.c new file mode 100644 index 0000000..07b7365 --- /dev/null +++ b/src/udev/udev-event.c @@ -0,0 +1,893 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <net/if.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.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 "format-util.h" +#include "libudev-util.h" +#include "netlink-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-node.h" +#include "udev-watch.h" +#include "udev.h" + +typedef struct Spawn { + const char *cmd; + pid_t pid; + usec_t timeout_warn_usec; + usec_t timeout_usec; + 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), + }; + + 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); + hashmap_free_free_key(event->run_list); + hashmap_free_free_free(event->seclabel_list); + free(event->program_result); + free(event->name); + + return mfree(event); +} + +enum subst_type { + SUBST_DEVNODE, + SUBST_ATTR, + SUBST_ENV, + SUBST_KERNEL, + SUBST_KERNEL_NUMBER, + SUBST_DRIVER, + SUBST_DEVPATH, + SUBST_ID, + SUBST_MAJOR, + SUBST_MINOR, + SUBST_RESULT, + SUBST_PARENT, + SUBST_NAME, + SUBST_LINKS, + SUBST_ROOT, + SUBST_SYS, +}; + +struct subst_map_entry { + const char *name; + const char fmt; + enum subst_type type; +}; + +static const struct subst_map_entry map[] = { + { .name = "devnode", .fmt = 'N', .type = SUBST_DEVNODE }, + { .name = "tempnode", .fmt = 'N', .type = SUBST_DEVNODE }, + { .name = "attr", .fmt = 's', .type = SUBST_ATTR }, + { .name = "sysfs", .fmt = 's', .type = SUBST_ATTR }, + { .name = "env", .fmt = 'E', .type = SUBST_ENV }, + { .name = "kernel", .fmt = 'k', .type = SUBST_KERNEL }, + { .name = "number", .fmt = 'n', .type = SUBST_KERNEL_NUMBER }, + { .name = "driver", .fmt = 'd', .type = SUBST_DRIVER }, + { .name = "devpath", .fmt = 'p', .type = SUBST_DEVPATH }, + { .name = "id", .fmt = 'b', .type = SUBST_ID }, + { .name = "major", .fmt = 'M', .type = SUBST_MAJOR }, + { .name = "minor", .fmt = 'm', .type = SUBST_MINOR }, + { .name = "result", .fmt = 'c', .type = SUBST_RESULT }, + { .name = "parent", .fmt = 'P', .type = SUBST_PARENT }, + { .name = "name", .fmt = 'D', .type = SUBST_NAME }, + { .name = "links", .fmt = 'L', .type = SUBST_LINKS }, + { .name = "root", .fmt = 'r', .type = SUBST_ROOT }, + { .name = "sys", .fmt = 'S', .type = SUBST_SYS }, +}; + +static ssize_t subst_format_var(UdevEvent *event, + const struct subst_map_entry *entry, char *attr, + char *dest, size_t l) { + sd_device *parent, *dev = event->dev; + const char *val = NULL; + char *s = dest; + dev_t devnum; + int r; + + assert(entry); + + switch (entry->type) { + case SUBST_DEVPATH: + r = sd_device_get_devpath(dev, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case SUBST_KERNEL: + r = sd_device_get_sysname(dev, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case SUBST_KERNEL_NUMBER: + r = sd_device_get_sysnum(dev, &val); + if (r < 0) + return r == -ENOENT ? 0 : r; + l = strpcpy(&s, l, val); + break; + case SUBST_ID: + if (!event->dev_parent) + return 0; + r = sd_device_get_sysname(event->dev_parent, &val); + if (r < 0) + return r; + l = strpcpy(&s, l, val); + break; + case SUBST_DRIVER: + if (!event->dev_parent) + return 0; + r = sd_device_get_driver(event->dev_parent, &val); + if (r < 0) + return r == -ENOENT ? 0 : r; + l = strpcpy(&s, l, val); + break; + case SUBST_MAJOR: + case SUBST_MINOR: { + char buf[DECIMAL_STR_MAX(unsigned)]; + + r = sd_device_get_devnum(dev, &devnum); + if (r < 0 && r != -ENOENT) + return r; + xsprintf(buf, "%u", r < 0 ? 0 : entry->type == SUBST_MAJOR ? major(devnum) : minor(devnum)); + l = strpcpy(&s, l, buf); + break; + } + case SUBST_RESULT: { + char *rest; + int i; + + if (!event->program_result) + return 0; + + /* get part of the result string */ + i = 0; + if (attr) + i = strtoul(attr, &rest, 10); + if (i > 0) { + char result[UTIL_PATH_SIZE], tmp[UTIL_PATH_SIZE], *cpos; + + strscpy(result, sizeof(result), event->program_result); + cpos = result; + while (--i) { + while (cpos[0] != '\0' && !isspace(cpos[0])) + cpos++; + while (isspace(cpos[0])) + cpos++; + if (cpos[0] == '\0') + break; + } + if (i > 0) { + log_error("requested part of result string not found"); + break; + } + strscpy(tmp, sizeof(tmp), cpos); + /* %{2+}c copies the whole string from the second part on */ + if (rest[0] != '+') { + cpos = strchr(tmp, ' '); + if (cpos) + cpos[0] = '\0'; + } + l = strpcpy(&s, l, tmp); + } else + l = strpcpy(&s, l, event->program_result); + break; + } + case SUBST_ATTR: { + char vbuf[UTIL_NAME_SIZE]; + size_t len; + int count; + + if (!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) + return 0; + + /* strip trailing whitespace, and replace unwanted characters */ + if (val != vbuf) + strscpy(vbuf, sizeof(vbuf), val); + len = strlen(vbuf); + while (len > 0 && isspace(vbuf[--len])) + vbuf[len] = '\0'; + 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 SUBST_PARENT: + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return r == -ENODEV ? 0 : r; + r = sd_device_get_devname(parent, &val); + if (r < 0) + return r == -ENOENT ? 0 : r; + l = strpcpy(&s, l, val + STRLEN("/dev/")); + break; + case SUBST_DEVNODE: + r = sd_device_get_devname(dev, &val); + if (r < 0) + return r == -ENOENT ? 0 : r; + l = strpcpy(&s, l, val); + break; + case 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 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); + break; + case SUBST_ROOT: + l = strpcpy(&s, l, "/dev"); + break; + case SUBST_SYS: + l = strpcpy(&s, l, "/sys"); + break; + case SUBST_ENV: + if (!attr) + return 0; + r = sd_device_get_property_value(dev, attr, &val); + if (r < 0) + return r == -ENOENT ? 0 : r; + l = strpcpy(&s, l, val); + break; + default: + assert_not_reached("Unknown format substitution type"); + } + + return s - dest; +} + +ssize_t udev_event_apply_format(UdevEvent *event, + const char *src, char *dest, size_t size, + bool replace_whitespace) { + const char *from; + char *s; + size_t l; + + assert(event); + assert(event->dev); + assert(src); + assert(dest); + assert(size > 0); + + from = src; + s = dest; + l = size; + + for (;;) { + const struct subst_map_entry *entry = NULL; + char attrbuf[UTIL_PATH_SIZE], *attr; + bool format_dollar = false; + ssize_t subst_len; + + while (from[0] != '\0') { + if (from[0] == '$') { + /* substitute named variable */ + unsigned i; + + if (from[1] == '$') { + from++; + goto copy; + } + + for (i = 0; i < ELEMENTSOF(map); i++) { + if (startswith(&from[1], map[i].name)) { + entry = &map[i]; + from += strlen(map[i].name)+1; + format_dollar = true; + goto subst; + } + } + } else if (from[0] == '%') { + /* substitute format char */ + unsigned i; + + if (from[1] == '%') { + from++; + goto copy; + } + + for (i = 0; i < ELEMENTSOF(map); i++) { + if (from[1] == map[i].fmt) { + entry = &map[i]; + from += 2; + goto subst; + } + } + } +copy: + /* copy char */ + if (l < 2) /* need space for this char and the terminating NUL */ + goto out; + s[0] = from[0]; + from++; + s++; + l--; + } + + goto out; +subst: + /* extract possible $format{attr} */ + if (from[0] == '{') { + unsigned i; + + from++; + for (i = 0; from[i] != '}'; i++) + if (from[i] == '\0') { + log_error("missing closing brace for format '%s'", src); + goto out; + } + + if (i >= sizeof(attrbuf)) + goto out; + memcpy(attrbuf, from, i); + attrbuf[i] = '\0'; + from += i+1; + attr = attrbuf; + } else + attr = NULL; + + subst_len = subst_format_var(event, entry, attr, s, l); + if (subst_len < 0) { + if (format_dollar) + log_device_warning_errno(event->dev, subst_len, "Failed to substitute variable '$%s', ignoring: %m", entry->name); + else + log_device_warning_errno(event->dev, subst_len, "Failed to apply format '%%%c', ignoring: %m", entry->fmt); + + continue; + } + + /* SUBST_RESULT handles spaces itself */ + if (replace_whitespace && entry->type != SUBST_RESULT) + /* util_replace_whitespace can replace in-place, + * and does nothing if subst_len == 0 + */ + subst_len = util_replace_whitespace(s, s, subst_len); + + s += subst_len; + l -= subst_len; + } + +out: + assert(l >= 1); + s[0] = '\0'; + return l; +} + +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; + + 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) + log_error_errno(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_debug("'%s'(%s) '%s'", spawn->cmd, + fd == spawn->fd_stdout ? "out" : "err", *q); + } + + 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, SIGKILL); + + log_error("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_warning("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_debug("Process '%s' succeeded.", spawn->cmd); + else + log_full(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_error("Process '%s' terminated by signal %s.", spawn->cmd, signal_to_string(si->si_status)); + break; + default: + log_error("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; + 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, NULL, spawn->fd_stdout, EPOLLIN, on_spawn_io, spawn); + if (r < 0) + return r; + } + + if (spawn->fd_stderr >= 0) { + r = sd_event_add_io(e, NULL, spawn->fd_stderr, EPOLLIN, on_spawn_io, spawn); + if (r < 0) + return r; + } + + r = sd_event_add_child(e, NULL, spawn->pid, WEXITED, on_spawn_sigchld, spawn); + if (r < 0) + return r; + + return sd_event_loop(e); +} + +int udev_event_spawn(UdevEvent *event, + usec_t timeout_usec, + 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_error_errno(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_error_errno(errno, "Failed to create pipe for command '%s': %m", cmd); + + argv = strv_split_full(cmd, NULL, SPLIT_QUOTES|SPLIT_RELAX); + if (!argv) + return log_oom(); + + if (isempty(argv[0])) + return log_error_errno(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_debug("Starting '%s'", cmd); + + r = safe_fork("(spawn)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return log_error_errno(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) { + .cmd = cmd, + .pid = pid, + .accept_failure = accept_failure, + .timeout_warn_usec = udev_warn_timeout(timeout_usec), + .timeout_usec = timeout_usec, + .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_error_errno(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 *action, *oldname; + char name[IFNAMSIZ]; + 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. */ + + r = sd_device_get_property_value(dev, "ACTION", &action); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get property 'ACTION': %m"); + + if (!streq(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"); + + strscpy(name, IFNAMSIZ, event->name); + r = rtnl_set_link_name(&event->rtnl, ifindex, name); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to rename network interface %i from '%s' to '%s': %m", ifindex, oldname, name); + + r = device_rename(dev, event->name); + if (r < 0) + return log_warning_errno(r, "Network interface %i is renamed from '%s' to '%s', but could not update sd_device object: %m", ifindex, oldname, name); + + log_device_debug(dev, "Network interface %i is renamed from '%s' to '%s'", ifindex, oldname, name); + + return 1; +} + +static int update_devnode(UdevEvent *event) { + sd_device *dev = event->dev; + const char *action; + bool apply; + 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 */ + if (event->dev_db_clone) + (void) udev_node_update_old_links(dev, event->dev_db_clone); + + if (!event->owner_set) { + 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 (!event->group_set) { + 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_set) { + 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 (r == -ENOENT) { + if (event->gid > 0) + /* default 0660 if a group is assigned */ + event->mode = 0660; + else + /* default 0600 */ + event->mode = 0600; + } + } + + r = sd_device_get_property_value(dev, "ACTION", &action); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get property 'ACTION': %m"); + + apply = streq(action, "add") || event->owner_set || event->group_set || event->mode_set; + return udev_node_add(dev, apply, event->mode, event->uid, event->gid, event->seclabel_list); +} + +static void event_execute_rules_on_remove( + UdevEvent *event, + usec_t timeout_usec, + 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, properties_list); + + if (sd_device_get_devnum(dev, NULL) >= 0) + (void) udev_node_remove(dev); +} + +int udev_event_execute_rules(UdevEvent *event, + usec_t timeout_usec, + Hashmap *properties_list, + UdevRules *rules) { + sd_device *dev = event->dev; + const char *subsystem, *action; + int r; + + assert(event); + assert(rules); + + r = sd_device_get_subsystem(dev, &subsystem); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get subsystem: %m"); + + r = sd_device_get_property_value(dev, "ACTION", &action); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get property 'ACTION': %m"); + + if (streq(action, "remove")) { + event_execute_rules_on_remove(event, timeout_usec, properties_list, rules); + return 0; + } + + r = device_clone_with_db(dev, &event->dev_db_clone); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to clone sd_device object, ignoring: %m"); + + if (event->dev_db_clone) { + r = sd_device_get_devnum(dev, NULL); + if (r < 0) { + if (r != -ENOENT) + log_device_debug_errno(dev, r, "Failed to get devnum, ignoring: %m"); + + if (streq(action, "move")) { + r = device_copy_properties(dev, event->dev_db_clone); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to copy properties from cloned device, ignoring: %m"); + } + } else + /* Disable watch during event processing. */ + (void) udev_watch_end(event->dev_db_clone); + } + + (void) udev_rules_apply_to_event(rules, event, timeout_usec, properties_list); + + (void) rename_netif(event); + (void) update_devnode(event); + + /* preserve old, or get new initialization timestamp */ + r = device_ensure_usec_initialized(dev, event->dev_db_clone); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to set initialization timestamp, ignoring: %m"); + + /* (re)write database file */ + r = device_tag_index(dev, event->dev_db_clone, true); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to update tags under /run/udev/tag/, ignoring: %m"); + + r = device_update_db(dev); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/, ignoring: %m"); + + device_set_is_initialized(dev); + + event->dev_db_clone = sd_device_unref(event->dev_db_clone); + + return 0; +} + +void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec) { + const char *cmd; + void *val; + Iterator i; + + HASHMAP_FOREACH_KEY(val, cmd, event->run_list, i) { + enum udev_builtin_cmd builtin_cmd = PTR_TO_INT(val); + char command[UTIL_PATH_SIZE]; + + udev_event_apply_format(event, cmd, command, sizeof(command), false); + + if (builtin_cmd >= 0 && builtin_cmd < _UDEV_BUILTIN_MAX) + udev_builtin_run(event->dev, builtin_cmd, command, false); + else { + if (event->exec_delay_usec > 0) { + log_debug("delay execution of '%s'", command); + (void) usleep(event->exec_delay_usec); + } + + (void) udev_event_spawn(event, timeout_usec, false, command, NULL, 0); + } + } +} diff --git a/src/udev/udev-node.c b/src/udev/udev-node.c new file mode 100644 index 0000000..1c00dd1 --- /dev/null +++ b/src/udev/udev-node.c @@ -0,0 +1,445 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <string.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 "stdio-util.h" +#include "string-util.h" +#include "strxcpyx.h" +#include "udev-node.h" + +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)) { + log_device_error(dev, "Conflicting device node '%s' found, link to '%s' will not be created.", slink, node); + return -EOPNOTSUPP; + } 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' failed: %m", slink_tmp, slink); + (void) unlink(slink_tmp); + } + + 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 *target = NULL, *filename = NULL, *dirname = NULL; + char name_enc[PATH_MAX]; + const char *id_filename; + int r; + + 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 && unlink(filename) == 0) + (void) rmdir(dirname); + + r = link_find_prioritized(dev, add, dirname, &target); + if (r < 0) { + log_device_debug(dev, "No reference left, removing '%s'", slink); + if (unlink(slink) == 0) + (void) rmdir_parents(slink, "/"); + } else + (void) node_symlink(dev, target, slink); + + if (add) + do { + _cleanup_close_ int fd = -1; + + r = mkdir_parents(filename, 0755); + if (!IN_SET(r, 0, -ENOENT)) + break; + fd = open(filename, O_WRONLY|O_CREAT|O_CLOEXEC|O_TRUNC|O_NOFOLLOW, 0444); + if (fd < 0) + r = -errno; + } while (r == -ENOENT); + + return r; +} + +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, + mode_t mode, uid_t uid, gid_t gid, + Hashmap *seclabel_list) { + const char *devnode, *subsystem, *id_filename = NULL; + struct stat stats; + dev_t devnum; + int r = 0; + + 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; + + if (lstat(devnode, &stats) < 0) + return log_device_debug_errno(dev, errno, "cannot stat() node '%s' (%m)", devnode); + + if (((stats.st_mode & S_IFMT) != (mode & S_IFMT)) || (stats.st_rdev != devnum)) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EEXIST), "Found node '%s' with non-matching devnum %s, skip handling", + devnode, id_filename); + + if (apply) { + bool selinux = false, smack = false; + const char *name, *label; + Iterator i; + + if ((stats.st_mode & 0777) != (mode & 0777) || stats.st_uid != uid || stats.st_gid != gid) { + log_device_debug(dev, "Setting permissions %s, %#o, uid=%u, gid=%u", devnode, mode, uid, gid); + if (chmod(devnode, mode) < 0) + r = log_device_warning_errno(dev, errno, "Failed to set mode of %s to %#o: %m", devnode, mode); + if (chown(devnode, uid, gid) < 0) + r = log_device_warning_errno(dev, errno, "Failed to set owner of %s to uid=%u, gid=%u: %m", devnode, uid, gid); + } else + log_device_debug(dev, "Preserve permissions of %s, %#o, uid=%u, gid=%u", devnode, mode, uid, gid); + + /* apply SECLABEL{$module}=$label */ + HASHMAP_FOREACH_KEY(label, name, seclabel_list, i) { + int q; + + if (streq(name, "selinux")) { + selinux = true; + + q = mac_selinux_apply(devnode, label); + if (q < 0) + log_device_error_errno(dev, 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(devnode, SMACK_ATTR_ACCESS, label); + if (q < 0) + log_device_error_errno(dev, 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(devnode, LABEL_IGNORE_ENOENT); + if (!smack) + (void) mac_smack_apply(devnode, SMACK_ATTR_ACCESS, NULL); + } + + /* always update timestamp when we re-use the node, like on media change events */ + (void) utimensat(AT_FDCWD, devnode, NULL, 0); + + 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, + Hashmap *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, mode=%#o, uid="UID_FMT", gid="GID_FMT, + devnode, strnull(id_filename), mode, uid, gid); + } + + 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) + (void) link_update(dev, devlink, true); + + 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) + (void) link_update(dev, devlink, false); + + 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..223c8f0 --- /dev/null +++ b/src/udev/udev-node.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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, + Hashmap *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..bc9c6c2 --- /dev/null +++ b/src/udev/udev-rules.c @@ -0,0 +1,2664 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <fnmatch.h> +#include <limits.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "conf-files.h" +#include "device-private.h" +#include "device-util.h" +#include "dirent-util.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "glob-util.h" +#include "libudev-util.h" +#include "mkdir.h" +#include "parse-util.h" +#include "path-util.h" +#include "proc-cmdline.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "strbuf.h" +#include "string-util.h" +#include "strv.h" +#include "strxcpyx.h" +#include "sysctl-util.h" +#include "udev-builtin.h" +#include "udev.h" +#include "user-util.h" +#include "util.h" + +#define PREALLOC_TOKEN 2048 + +struct uid_gid { + unsigned name_off; + union { + uid_t uid; + gid_t gid; + }; +}; + +static const char* const rules_dirs[] = { + "/etc/udev/rules.d", + "/run/udev/rules.d", + UDEVLIBEXECDIR "/rules.d", + NULL +}; + +struct UdevRules { + usec_t dirs_ts_usec; + ResolveNameTiming resolve_name_timing; + + /* every key in the rules file becomes a token */ + struct token *tokens; + unsigned token_cur; + unsigned token_max; + + /* all key strings are copied and de-duplicated in a single continuous string buffer */ + struct strbuf *strbuf; + + /* during rule parsing, uid/gid lookup results are cached */ + struct uid_gid *uids; + unsigned uids_cur; + unsigned uids_max; + struct uid_gid *gids; + unsigned gids_cur; + unsigned gids_max; +}; + +static char *rules_str(UdevRules *rules, unsigned off) { + return rules->strbuf->buf + off; +} + +static unsigned rules_add_string(UdevRules *rules, const char *s) { + return strbuf_add_string(rules->strbuf, s, strlen(s)); +} + +/* KEY=="", KEY!="", KEY+="", KEY-="", KEY="", KEY:="" */ +enum operation_type { + OP_UNSET, + + OP_MATCH, + OP_NOMATCH, + OP_MATCH_MAX, + + OP_ADD, + OP_REMOVE, + OP_ASSIGN, + OP_ASSIGN_FINAL, +}; + +enum string_glob_type { + GL_UNSET, + GL_PLAIN, /* no special chars */ + GL_GLOB, /* shell globs ?,*,[] */ + GL_SPLIT, /* multi-value A|B */ + GL_SPLIT_GLOB, /* multi-value with glob A*|B* */ + GL_SOMETHING, /* commonly used "?*" */ +}; + +enum string_subst_type { + SB_UNSET, + SB_NONE, + SB_FORMAT, + SB_SUBSYS, +}; + +/* tokens of a rule are sorted/handled in this order */ +enum token_type { + TK_UNSET, + TK_RULE, + + TK_M_ACTION, /* val */ + TK_M_DEVPATH, /* val */ + TK_M_KERNEL, /* val */ + TK_M_DEVLINK, /* val */ + TK_M_NAME, /* val */ + TK_M_ENV, /* val, attr */ + TK_M_TAG, /* val */ + TK_M_SUBSYSTEM, /* val */ + TK_M_DRIVER, /* val */ + TK_M_WAITFOR, /* val */ + TK_M_ATTR, /* val, attr */ + TK_M_SYSCTL, /* val, attr */ + + TK_M_PARENTS_MIN, + TK_M_KERNELS, /* val */ + TK_M_SUBSYSTEMS, /* val */ + TK_M_DRIVERS, /* val */ + TK_M_ATTRS, /* val, attr */ + TK_M_TAGS, /* val */ + TK_M_PARENTS_MAX, + + TK_M_TEST, /* val, mode_t */ + TK_M_PROGRAM, /* val */ + TK_M_IMPORT_FILE, /* val */ + TK_M_IMPORT_PROG, /* val */ + TK_M_IMPORT_BUILTIN, /* val */ + TK_M_IMPORT_DB, /* val */ + TK_M_IMPORT_CMDLINE, /* val */ + TK_M_IMPORT_PARENT, /* val */ + TK_M_RESULT, /* val */ + TK_M_MAX, + + TK_A_STRING_ESCAPE_NONE, + TK_A_STRING_ESCAPE_REPLACE, + TK_A_DB_PERSIST, + TK_A_INOTIFY_WATCH, /* int */ + TK_A_DEVLINK_PRIO, /* int */ + TK_A_OWNER, /* val */ + TK_A_GROUP, /* val */ + TK_A_MODE, /* val */ + TK_A_OWNER_ID, /* uid_t */ + TK_A_GROUP_ID, /* gid_t */ + TK_A_MODE_ID, /* mode_t */ + TK_A_TAG, /* val */ + TK_A_STATIC_NODE, /* val */ + TK_A_SECLABEL, /* val, attr */ + TK_A_ENV, /* val, attr */ + TK_A_NAME, /* val */ + TK_A_DEVLINK, /* val */ + TK_A_ATTR, /* val, attr */ + TK_A_SYSCTL, /* val, attr */ + TK_A_RUN_BUILTIN, /* val, bool */ + TK_A_RUN_PROGRAM, /* val, bool */ + TK_A_GOTO, /* size_t */ + + TK_END, +}; + +/* we try to pack stuff in a way that we take only 12 bytes per token */ +struct token { + union { + unsigned char type; /* same in rule and key */ + struct { + enum token_type type:8; + bool can_set_name:1; + bool has_static_node:1; + unsigned unused:6; + unsigned short token_count; + unsigned label_off; + unsigned short filename_off; + unsigned short filename_line; + } rule; + struct { + enum token_type type:8; + enum operation_type op:8; + enum string_glob_type glob:8; + enum string_subst_type subst:4; + enum string_subst_type attrsubst:4; + unsigned value_off; + union { + unsigned attr_off; + unsigned rule_goto; + mode_t mode; + uid_t uid; + gid_t gid; + int devlink_prio; + int watch; + enum udev_builtin_cmd builtin_cmd; + }; + } key; + }; +}; + +#define MAX_TK 64 +struct rule_tmp { + UdevRules *rules; + struct token rule; + struct token token[MAX_TK]; + unsigned token_cur; +}; + +#if ENABLE_DEBUG_UDEV +static const char *operation_str(enum operation_type type) { + static const char *operation_strs[] = { + [OP_UNSET] = "UNSET", + [OP_MATCH] = "match", + [OP_NOMATCH] = "nomatch", + [OP_MATCH_MAX] = "MATCH_MAX", + + [OP_ADD] = "add", + [OP_REMOVE] = "remove", + [OP_ASSIGN] = "assign", + [OP_ASSIGN_FINAL] = "assign-final", + }; + + return operation_strs[type]; +} + +static const char *string_glob_str(enum string_glob_type type) { + static const char *string_glob_strs[] = { + [GL_UNSET] = "UNSET", + [GL_PLAIN] = "plain", + [GL_GLOB] = "glob", + [GL_SPLIT] = "split", + [GL_SPLIT_GLOB] = "split-glob", + [GL_SOMETHING] = "split-glob", + }; + + return string_glob_strs[type]; +} + +static const char *token_str(enum token_type type) { + static const char *token_strs[] = { + [TK_UNSET] = "UNSET", + [TK_RULE] = "RULE", + + [TK_M_ACTION] = "M ACTION", + [TK_M_DEVPATH] = "M DEVPATH", + [TK_M_KERNEL] = "M KERNEL", + [TK_M_DEVLINK] = "M DEVLINK", + [TK_M_NAME] = "M NAME", + [TK_M_ENV] = "M ENV", + [TK_M_TAG] = "M TAG", + [TK_M_SUBSYSTEM] = "M SUBSYSTEM", + [TK_M_DRIVER] = "M DRIVER", + [TK_M_WAITFOR] = "M WAITFOR", + [TK_M_ATTR] = "M ATTR", + [TK_M_SYSCTL] = "M SYSCTL", + + [TK_M_PARENTS_MIN] = "M PARENTS_MIN", + [TK_M_KERNELS] = "M KERNELS", + [TK_M_SUBSYSTEMS] = "M SUBSYSTEMS", + [TK_M_DRIVERS] = "M DRIVERS", + [TK_M_ATTRS] = "M ATTRS", + [TK_M_TAGS] = "M TAGS", + [TK_M_PARENTS_MAX] = "M PARENTS_MAX", + + [TK_M_TEST] = "M TEST", + [TK_M_PROGRAM] = "M PROGRAM", + [TK_M_IMPORT_FILE] = "M IMPORT_FILE", + [TK_M_IMPORT_PROG] = "M IMPORT_PROG", + [TK_M_IMPORT_BUILTIN] = "M IMPORT_BUILTIN", + [TK_M_IMPORT_DB] = "M IMPORT_DB", + [TK_M_IMPORT_CMDLINE] = "M IMPORT_CMDLINE", + [TK_M_IMPORT_PARENT] = "M IMPORT_PARENT", + [TK_M_RESULT] = "M RESULT", + [TK_M_MAX] = "M MAX", + + [TK_A_STRING_ESCAPE_NONE] = "A STRING_ESCAPE_NONE", + [TK_A_STRING_ESCAPE_REPLACE] = "A STRING_ESCAPE_REPLACE", + [TK_A_DB_PERSIST] = "A DB_PERSIST", + [TK_A_INOTIFY_WATCH] = "A INOTIFY_WATCH", + [TK_A_DEVLINK_PRIO] = "A DEVLINK_PRIO", + [TK_A_OWNER] = "A OWNER", + [TK_A_GROUP] = "A GROUP", + [TK_A_MODE] = "A MODE", + [TK_A_OWNER_ID] = "A OWNER_ID", + [TK_A_GROUP_ID] = "A GROUP_ID", + [TK_A_STATIC_NODE] = "A STATIC_NODE", + [TK_A_SECLABEL] = "A SECLABEL", + [TK_A_MODE_ID] = "A MODE_ID", + [TK_A_ENV] = "A ENV", + [TK_A_TAG] = "A ENV", + [TK_A_NAME] = "A NAME", + [TK_A_DEVLINK] = "A DEVLINK", + [TK_A_ATTR] = "A ATTR", + [TK_A_SYSCTL] = "A SYSCTL", + [TK_A_RUN_BUILTIN] = "A RUN_BUILTIN", + [TK_A_RUN_PROGRAM] = "A RUN_PROGRAM", + [TK_A_GOTO] = "A GOTO", + + [TK_END] = "END", + }; + + return token_strs[type]; +} + +static void dump_token(UdevRules *rules, struct token *token) { + enum token_type type = token->type; + enum operation_type op = token->key.op; + enum string_glob_type glob = token->key.glob; + const char *value = rules_str(rules, token->key.value_off); + const char *attr = &rules->strbuf->buf[token->key.attr_off]; + + switch (type) { + case TK_RULE: + { + const char *tks_ptr = (char *)rules->tokens; + const char *tk_ptr = (char *)token; + unsigned idx = (tk_ptr - tks_ptr) / sizeof(struct token); + + log_debug("* RULE %s:%u, token: %u, count: %u, label: '%s'", + &rules->strbuf->buf[token->rule.filename_off], token->rule.filename_line, + idx, token->rule.token_count, + &rules->strbuf->buf[token->rule.label_off]); + break; + } + case TK_M_ACTION: + case TK_M_DEVPATH: + case TK_M_KERNEL: + case TK_M_SUBSYSTEM: + case TK_M_DRIVER: + case TK_M_WAITFOR: + case TK_M_DEVLINK: + case TK_M_NAME: + case TK_M_KERNELS: + case TK_M_SUBSYSTEMS: + case TK_M_DRIVERS: + case TK_M_TAGS: + case TK_M_PROGRAM: + case TK_M_IMPORT_FILE: + case TK_M_IMPORT_PROG: + case TK_M_IMPORT_DB: + case TK_M_IMPORT_CMDLINE: + case TK_M_IMPORT_PARENT: + case TK_M_RESULT: + case TK_A_NAME: + case TK_A_DEVLINK: + case TK_A_OWNER: + case TK_A_GROUP: + case TK_A_MODE: + case TK_A_RUN_BUILTIN: + case TK_A_RUN_PROGRAM: + log_debug("%s %s '%s'(%s)", + token_str(type), operation_str(op), value, string_glob_str(glob)); + break; + case TK_M_IMPORT_BUILTIN: + log_debug("%s %i '%s'", token_str(type), token->key.builtin_cmd, value); + break; + case TK_M_ATTR: + case TK_M_SYSCTL: + case TK_M_ATTRS: + case TK_M_ENV: + case TK_A_ATTR: + case TK_A_SYSCTL: + case TK_A_ENV: + log_debug("%s %s '%s' '%s'(%s)", + token_str(type), operation_str(op), attr, value, string_glob_str(glob)); + break; + case TK_M_TAG: + case TK_A_TAG: + log_debug("%s %s '%s'", token_str(type), operation_str(op), value); + break; + case TK_A_STRING_ESCAPE_NONE: + case TK_A_STRING_ESCAPE_REPLACE: + case TK_A_DB_PERSIST: + log_debug("%s", token_str(type)); + break; + case TK_M_TEST: + log_debug("%s %s '%s'(%s) %#o", + token_str(type), operation_str(op), value, string_glob_str(glob), token->key.mode); + break; + case TK_A_INOTIFY_WATCH: + log_debug("%s %u", token_str(type), token->key.watch); + break; + case TK_A_DEVLINK_PRIO: + log_debug("%s %u", token_str(type), token->key.devlink_prio); + break; + case TK_A_OWNER_ID: + log_debug("%s %s %u", token_str(type), operation_str(op), token->key.uid); + break; + case TK_A_GROUP_ID: + log_debug("%s %s %u", token_str(type), operation_str(op), token->key.gid); + break; + case TK_A_MODE_ID: + log_debug("%s %s %#o", token_str(type), operation_str(op), token->key.mode); + break; + case TK_A_STATIC_NODE: + log_debug("%s '%s'", token_str(type), value); + break; + case TK_A_SECLABEL: + log_debug("%s %s '%s' '%s'", token_str(type), operation_str(op), attr, value); + break; + case TK_A_GOTO: + log_debug("%s '%s' %u", token_str(type), value, token->key.rule_goto); + break; + case TK_END: + log_debug("* %s", token_str(type)); + break; + case TK_M_PARENTS_MIN: + case TK_M_PARENTS_MAX: + case TK_M_MAX: + case TK_UNSET: + log_debug("Unknown token type %u", type); + break; + } +} + +static void dump_rules(UdevRules *rules) { + unsigned i; + + log_debug("Dumping %u (%zu bytes) tokens, %zu (%zu bytes) strings", + rules->token_cur, + rules->token_cur * sizeof(struct token), + rules->strbuf->nodes_count, + rules->strbuf->len); + for (i = 0; i < rules->token_cur; i++) + dump_token(rules, &rules->tokens[i]); +} +#else +static void dump_token(UdevRules *rules, struct token *token) {} +static void dump_rules(UdevRules *rules) {} +#endif /* ENABLE_DEBUG_UDEV */ + +static int add_token(UdevRules *rules, struct token *token) { + /* grow buffer if needed */ + if (rules->token_cur+1 >= rules->token_max) { + struct token *tokens; + unsigned add; + + /* double the buffer size */ + add = rules->token_max; + if (add < 8) + add = 8; + + tokens = reallocarray(rules->tokens, rules->token_max + add, sizeof(struct token)); + if (!tokens) + return -1; + rules->tokens = tokens; + rules->token_max += add; + } + memcpy(&rules->tokens[rules->token_cur], token, sizeof(struct token)); + rules->token_cur++; + return 0; +} + +static void log_unknown_owner(sd_device *dev, int error, const char *entity, const char *owner) { + if (IN_SET(abs(error), ENOENT, ESRCH)) + log_device_error(dev, "Specified %s '%s' unknown", entity, owner); + else + log_device_error_errno(dev, error, "Failed to resolve %s '%s': %m", entity, owner); +} + +static uid_t add_uid(UdevRules *rules, const char *owner) { + unsigned i; + uid_t uid = 0; + unsigned off; + int r; + + /* lookup, if we know it already */ + for (i = 0; i < rules->uids_cur; i++) { + off = rules->uids[i].name_off; + if (streq(rules_str(rules, off), owner)) { + uid = rules->uids[i].uid; + return uid; + } + } + r = get_user_creds(&owner, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) + log_unknown_owner(NULL, r, "user", owner); + + /* grow buffer if needed */ + if (rules->uids_cur+1 >= rules->uids_max) { + struct uid_gid *uids; + unsigned add; + + /* double the buffer size */ + add = rules->uids_max; + if (add < 1) + add = 8; + + uids = reallocarray(rules->uids, rules->uids_max + add, sizeof(struct uid_gid)); + if (!uids) + return uid; + rules->uids = uids; + rules->uids_max += add; + } + rules->uids[rules->uids_cur].uid = uid; + off = rules_add_string(rules, owner); + if (off <= 0) + return uid; + rules->uids[rules->uids_cur].name_off = off; + rules->uids_cur++; + return uid; +} + +static gid_t add_gid(UdevRules *rules, const char *group) { + unsigned i; + gid_t gid = 0; + unsigned off; + int r; + + /* lookup, if we know it already */ + for (i = 0; i < rules->gids_cur; i++) { + off = rules->gids[i].name_off; + if (streq(rules_str(rules, off), group)) { + gid = rules->gids[i].gid; + return gid; + } + } + r = get_group_creds(&group, &gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) + log_unknown_owner(NULL, r, "group", group); + + /* grow buffer if needed */ + if (rules->gids_cur+1 >= rules->gids_max) { + struct uid_gid *gids; + unsigned add; + + /* double the buffer size */ + add = rules->gids_max; + if (add < 1) + add = 8; + + gids = reallocarray(rules->gids, rules->gids_max + add, sizeof(struct uid_gid)); + if (!gids) + return gid; + rules->gids = gids; + rules->gids_max += add; + } + rules->gids[rules->gids_cur].gid = gid; + off = rules_add_string(rules, group); + if (off <= 0) + return gid; + rules->gids[rules->gids_cur].name_off = off; + rules->gids_cur++; + return gid; +} + +static int import_property_from_string(sd_device *dev, char *line) { + char *key; + char *val; + size_t len; + + /* find key */ + key = line; + while (isspace(key[0])) + key++; + + /* comment or empty line */ + if (IN_SET(key[0], '#', '\0')) + return 0; + + /* split key/value */ + val = strchr(key, '='); + if (!val) + return -EINVAL; + val[0] = '\0'; + val++; + + /* find value */ + while (isspace(val[0])) + val++; + + /* terminate key */ + len = strlen(key); + if (len == 0) + return -EINVAL; + while (isspace(key[len-1])) + len--; + key[len] = '\0'; + + /* terminate value */ + len = strlen(val); + if (len == 0) + return -EINVAL; + while (isspace(val[len-1])) + len--; + val[len] = '\0'; + + if (len == 0) + return -EINVAL; + + /* unquote */ + if (IN_SET(val[0], '"', '\'')) { + if (len == 1 || val[len-1] != val[0]) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Inconsistent quoting: '%s', skip", + line); + val[len-1] = '\0'; + val++; + } + + return device_add_property(dev, key, val); +} + +static int import_file_into_properties(sd_device *dev, const char *filename) { + _cleanup_fclose_ FILE *f = NULL; + int r; + + f = fopen(filename, "re"); + if (!f) + return -errno; + + for (;;) { + _cleanup_free_ char *line = NULL; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return r; + if (r == 0) + break; + + (void) import_property_from_string(dev, line); + } + + return 0; +} + +static int import_program_into_properties(UdevEvent *event, + usec_t timeout_usec, + const char *program) { + char result[UTIL_LINE_SIZE]; + char *line; + int r; + + r = udev_event_spawn(event, timeout_usec, false, program, result, sizeof result); + if (r < 0) + return r; + if (r > 0) + return -EIO; + + line = result; + while (line) { + char *pos; + + pos = strchr(line, '\n'); + if (pos) { + pos[0] = '\0'; + pos = &pos[1]; + } + (void) import_property_from_string(event->dev, line); + line = pos; + } + return 0; +} + +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 < 0) + return r; + + FOREACH_DEVICE_PROPERTY(parent, key, val) + if (fnmatch(filter, key, 0) == 0) + device_add_property(dev, key, val); + return 0; +} + +static void attr_subst_subdir(char *attr, size_t len) { + const char *pos, *tail, *path; + _cleanup_closedir_ DIR *dir = NULL; + struct dirent *dent; + + pos = strstr(attr, "/*/"); + if (!pos) + return; + + tail = pos + 2; + path = strndupa(attr, pos - attr + 1); /* include slash at end */ + dir = opendir(path); + if (!dir) + return; + + FOREACH_DIRENT_ALL(dent, dir, break) + if (dent->d_name[0] != '.') { + char n[strlen(dent->d_name) + strlen(tail) + 1]; + + strscpyl(n, sizeof n, dent->d_name, tail, NULL); + if (faccessat(dirfd(dir), n, F_OK, 0) == 0) { + strscpyl(attr, len, path, n, NULL); + break; + } + } +} + +static int get_key(char **line, char **key, enum operation_type *op, char **value) { + char *linepos; + char *temp; + unsigned i, j; + + linepos = *line; + if (!linepos || linepos[0] == '\0') + return -1; + + /* skip whitespace */ + while (isspace(linepos[0]) || linepos[0] == ',') + linepos++; + + /* get the key */ + if (linepos[0] == '\0') + return -1; + *key = linepos; + + for (;;) { + linepos++; + if (linepos[0] == '\0') + return -1; + if (isspace(linepos[0])) + break; + if (linepos[0] == '=') + break; + if (IN_SET(linepos[0], '+', '-', '!', ':')) + if (linepos[1] == '=') + break; + } + + /* remember end of key */ + temp = linepos; + + /* skip whitespace after key */ + while (isspace(linepos[0])) + linepos++; + if (linepos[0] == '\0') + return -1; + + /* get operation type */ + if (linepos[0] == '=' && linepos[1] == '=') { + *op = OP_MATCH; + linepos += 2; + } else if (linepos[0] == '!' && linepos[1] == '=') { + *op = OP_NOMATCH; + linepos += 2; + } else if (linepos[0] == '+' && linepos[1] == '=') { + *op = OP_ADD; + linepos += 2; + } else if (linepos[0] == '-' && linepos[1] == '=') { + *op = OP_REMOVE; + linepos += 2; + } else if (linepos[0] == '=') { + *op = OP_ASSIGN; + linepos++; + } else if (linepos[0] == ':' && linepos[1] == '=') { + *op = OP_ASSIGN_FINAL; + linepos += 2; + } else + return -1; + + /* terminate key */ + temp[0] = '\0'; + + /* skip whitespace after operator */ + while (isspace(linepos[0])) + linepos++; + if (linepos[0] == '\0') + return -1; + + /* get the value */ + if (linepos[0] == '"') + linepos++; + else + return -1; + *value = linepos; + + /* terminate */ + for (i = 0, j = 0; ; i++, j++) { + + if (linepos[i] == '"') + break; + + if (linepos[i] == '\0') + return -1; + + /* double quotes can be escaped */ + if (linepos[i] == '\\') + if (linepos[i+1] == '"') + i++; + + linepos[j] = linepos[i]; + } + linepos[j] = '\0'; + + /* move line to next key */ + *line = linepos + i + 1; + return 0; +} + +/* extract possible KEY{attr} */ +static const char *get_key_attribute(char *str) { + char *pos; + char *attr; + + attr = strchr(str, '{'); + if (attr) { + attr++; + pos = strchr(attr, '}'); + if (!pos) { + log_error("Missing closing brace for format"); + return NULL; + } + pos[0] = '\0'; + return attr; + } + return NULL; +} + +static int rule_add_key(struct rule_tmp *rule_tmp, enum token_type type, + enum operation_type op, + const char *value, const void *data) { + struct token *token = rule_tmp->token + rule_tmp->token_cur; + const char *attr = NULL; + + if (rule_tmp->token_cur >= ELEMENTSOF(rule_tmp->token)) + return -E2BIG; + + memzero(token, sizeof(struct token)); + + switch (type) { + case TK_M_ACTION: + case TK_M_DEVPATH: + case TK_M_KERNEL: + case TK_M_SUBSYSTEM: + case TK_M_DRIVER: + case TK_M_WAITFOR: + case TK_M_DEVLINK: + case TK_M_NAME: + case TK_M_KERNELS: + case TK_M_SUBSYSTEMS: + case TK_M_DRIVERS: + case TK_M_TAGS: + case TK_M_PROGRAM: + case TK_M_IMPORT_FILE: + case TK_M_IMPORT_PROG: + case TK_M_IMPORT_DB: + case TK_M_IMPORT_CMDLINE: + case TK_M_IMPORT_PARENT: + case TK_M_RESULT: + case TK_A_OWNER: + case TK_A_GROUP: + case TK_A_MODE: + case TK_A_DEVLINK: + case TK_A_NAME: + case TK_A_GOTO: + case TK_M_TAG: + case TK_A_TAG: + case TK_A_STATIC_NODE: + token->key.value_off = rules_add_string(rule_tmp->rules, value); + break; + case TK_M_IMPORT_BUILTIN: + token->key.value_off = rules_add_string(rule_tmp->rules, value); + token->key.builtin_cmd = *(enum udev_builtin_cmd *)data; + break; + case TK_M_ENV: + case TK_M_ATTR: + case TK_M_SYSCTL: + case TK_M_ATTRS: + case TK_A_ATTR: + case TK_A_SYSCTL: + case TK_A_ENV: + case TK_A_SECLABEL: + attr = data; + token->key.value_off = rules_add_string(rule_tmp->rules, value); + token->key.attr_off = rules_add_string(rule_tmp->rules, attr); + break; + case TK_M_TEST: + token->key.value_off = rules_add_string(rule_tmp->rules, value); + if (data) + token->key.mode = *(mode_t *)data; + break; + case TK_A_STRING_ESCAPE_NONE: + case TK_A_STRING_ESCAPE_REPLACE: + case TK_A_DB_PERSIST: + break; + case TK_A_RUN_BUILTIN: + case TK_A_RUN_PROGRAM: + token->key.builtin_cmd = *(enum udev_builtin_cmd *)data; + token->key.value_off = rules_add_string(rule_tmp->rules, value); + break; + case TK_A_INOTIFY_WATCH: + case TK_A_DEVLINK_PRIO: + token->key.devlink_prio = *(int *)data; + break; + case TK_A_OWNER_ID: + token->key.uid = *(uid_t *)data; + break; + case TK_A_GROUP_ID: + token->key.gid = *(gid_t *)data; + break; + case TK_A_MODE_ID: + token->key.mode = *(mode_t *)data; + break; + case TK_RULE: + case TK_M_PARENTS_MIN: + case TK_M_PARENTS_MAX: + case TK_M_MAX: + case TK_END: + case TK_UNSET: + assert_not_reached("wrong type"); + } + + if (value && type < TK_M_MAX) { + /* check if we need to split or call fnmatch() while matching rules */ + enum string_glob_type glob; + bool has_split, has_glob; + + has_split = strchr(value, '|'); + has_glob = string_is_glob(value); + if (has_split && has_glob) + glob = GL_SPLIT_GLOB; + else if (has_split) + glob = GL_SPLIT; + else if (has_glob) { + if (streq(value, "?*")) + glob = GL_SOMETHING; + else + glob = GL_GLOB; + } else + glob = GL_PLAIN; + + token->key.glob = glob; + } + + if (value && type > TK_M_MAX) { + /* check if assigned value has substitution chars */ + if (value[0] == '[') + token->key.subst = SB_SUBSYS; + else if (strchr(value, '%') || strchr(value, '$')) + token->key.subst = SB_FORMAT; + else + token->key.subst = SB_NONE; + } + + if (attr) { + /* check if property/attribute name has substitution chars */ + if (attr[0] == '[') + token->key.attrsubst = SB_SUBSYS; + else if (strchr(attr, '%') || strchr(attr, '$')) + token->key.attrsubst = SB_FORMAT; + else + token->key.attrsubst = SB_NONE; + } + + token->key.type = type; + token->key.op = op; + rule_tmp->token_cur++; + + return 0; +} + +static int sort_token(UdevRules *rules, struct rule_tmp *rule_tmp) { + unsigned i; + unsigned start = 0; + unsigned end = rule_tmp->token_cur; + + for (i = 0; i < rule_tmp->token_cur; i++) { + enum token_type next_val = TK_UNSET; + unsigned next_idx = 0; + unsigned j; + + /* find smallest value */ + for (j = start; j < end; j++) { + if (rule_tmp->token[j].type == TK_UNSET) + continue; + if (next_val == TK_UNSET || rule_tmp->token[j].type < next_val) { + next_val = rule_tmp->token[j].type; + next_idx = j; + } + } + + /* add token and mark done */ + if (add_token(rules, &rule_tmp->token[next_idx]) != 0) + return -1; + rule_tmp->token[next_idx].type = TK_UNSET; + + /* shrink range */ + if (next_idx == start) + start++; + if (next_idx+1 == end) + end--; + } + return 0; +} + +#define LOG_RULE_FULL(level, fmt, ...) log_full(level, "%s:%u: " fmt, filename, lineno, ##__VA_ARGS__) +#define LOG_RULE_ERROR(fmt, ...) LOG_RULE_FULL(LOG_ERR, fmt, ##__VA_ARGS__) +#define LOG_RULE_WARNING(fmt, ...) LOG_RULE_FULL(LOG_WARNING, fmt, ##__VA_ARGS__) +#define LOG_RULE_DEBUG(fmt, ...) LOG_RULE_FULL(LOG_DEBUG, fmt, ##__VA_ARGS__) +#define LOG_AND_RETURN(fmt, ...) { LOG_RULE_ERROR(fmt, __VA_ARGS__); return; } +#define LOG_AND_RETURN_ADD_KEY LOG_AND_RETURN("Temporary rule array too small, aborting event processing with %u items", rule_tmp.token_cur); + +static void add_rule(UdevRules *rules, char *line, + const char *filename, unsigned filename_off, unsigned lineno) { + char *linepos; + const char *attr; + struct rule_tmp rule_tmp = { + .rules = rules, + .rule.type = TK_RULE, + }; + int r; + + /* the offset in the rule is limited to unsigned short */ + if (filename_off < USHRT_MAX) + rule_tmp.rule.rule.filename_off = filename_off; + rule_tmp.rule.rule.filename_line = lineno; + + linepos = line; + for (;;) { + char *key; + char *value; + enum operation_type op; + + if (get_key(&linepos, &key, &op, &value) != 0) { + /* Avoid erroring on trailing whitespace. This is probably rare + * so save the work for the error case instead of always trying + * to strip the trailing whitespace with strstrip(). */ + while (isblank(*linepos)) + linepos++; + + /* If we aren't at the end of the line, this is a parsing error. + * Make a best effort to describe where the problem is. */ + if (!strchr(NEWLINE, *linepos)) { + char buf[2] = {*linepos}; + _cleanup_free_ char *tmp; + + tmp = cescape(buf); + LOG_RULE_ERROR("Invalid key/value pair, starting at character %tu ('%s')", linepos - line + 1, tmp); + if (*linepos == '#') + LOG_RULE_ERROR("Hint: comments can only start at beginning of line"); + } + break; + } + + if (streq(key, "ACTION")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_ACTION, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "DEVPATH")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_DEVPATH, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "KERNEL")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_KERNEL, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "SUBSYSTEM")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + /* bus, class, subsystem events should all be the same */ + if (STR_IN_SET(value, "subsystem", "bus", "class")) { + if (!streq(value, "subsystem")) + LOG_RULE_WARNING("'%s' must be specified as 'subsystem'; please fix", value); + + r = rule_add_key(&rule_tmp, TK_M_SUBSYSTEM, op, "subsystem|class|bus", NULL); + } else + r = rule_add_key(&rule_tmp, TK_M_SUBSYSTEM, op, value, NULL); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "DRIVER")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_DRIVER, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "ATTR{")) { + attr = get_key_attribute(key + STRLEN("ATTR")); + if (!attr) + LOG_AND_RETURN("Failed to parse %s attribute", "ATTR"); + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", "ATTR"); + + if (op < OP_MATCH_MAX) + r = rule_add_key(&rule_tmp, TK_M_ATTR, op, value, attr); + else + r = rule_add_key(&rule_tmp, TK_A_ATTR, op, value, attr); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "SYSCTL{")) { + attr = get_key_attribute(key + STRLEN("SYSCTL")); + if (!attr) + LOG_AND_RETURN("Failed to parse %s attribute", "ATTR"); + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", "ATTR"); + + if (op < OP_MATCH_MAX) + r = rule_add_key(&rule_tmp, TK_M_SYSCTL, op, value, attr); + else + r = rule_add_key(&rule_tmp, TK_A_SYSCTL, op, value, attr); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "SECLABEL{")) { + attr = get_key_attribute(key + STRLEN("SECLABEL")); + if (!attr) + LOG_AND_RETURN("Failed to parse %s attribute", "SECLABEL"); + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", "SECLABEL"); + + if (rule_add_key(&rule_tmp, TK_A_SECLABEL, op, value, attr) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "KERNELS")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_KERNELS, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "SUBSYSTEMS")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_SUBSYSTEMS, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "DRIVERS")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_DRIVERS, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "ATTRS{")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", "ATTRS"); + + attr = get_key_attribute(key + STRLEN("ATTRS")); + if (!attr) + LOG_AND_RETURN("Failed to parse %s attribute", "ATTRS"); + + if (startswith(attr, "device/")) + LOG_RULE_WARNING("'device' link may not be available in future kernels; please fix"); + if (strstr(attr, "../")) + LOG_RULE_WARNING("Direct reference to parent sysfs directory, may break in future kernels; please fix"); + if (rule_add_key(&rule_tmp, TK_M_ATTRS, op, value, attr) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "TAGS")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_TAGS, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "ENV{")) { + attr = get_key_attribute(key + STRLEN("ENV")); + if (!attr) + LOG_AND_RETURN("Failed to parse %s attribute", "ENV"); + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", "ENV"); + + if (op < OP_MATCH_MAX) + r = rule_add_key(&rule_tmp, TK_M_ENV, op, value, attr); + else { + if (STR_IN_SET(attr, + "ACTION", + "SUBSYSTEM", + "DEVTYPE", + "MAJOR", + "MINOR", + "DRIVER", + "IFINDEX", + "DEVNAME", + "DEVLINKS", + "DEVPATH", + "TAGS")) + LOG_AND_RETURN("Invalid ENV attribute, '%s' cannot be set", attr); + + r = rule_add_key(&rule_tmp, TK_A_ENV, op, value, attr); + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "TAG")) { + if (op < OP_MATCH_MAX) + r = rule_add_key(&rule_tmp, TK_M_TAG, op, value, NULL); + else + r = rule_add_key(&rule_tmp, TK_A_TAG, op, value, NULL); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "PROGRAM")) { + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_PROGRAM, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "RESULT")) { + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_M_RESULT, op, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "IMPORT")) { + attr = get_key_attribute(key + STRLEN("IMPORT")); + if (!attr) { + LOG_RULE_WARNING("Ignoring IMPORT{} with missing type"); + continue; + } + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", "IMPORT"); + + if (streq(attr, "program")) { + /* find known built-in command */ + if (value[0] != '/') { + const enum udev_builtin_cmd cmd = udev_builtin_lookup(value); + + if (cmd >= 0) { + LOG_RULE_DEBUG("IMPORT found builtin '%s', replacing", value); + if (rule_add_key(&rule_tmp, TK_M_IMPORT_BUILTIN, op, value, &cmd) < 0) + LOG_AND_RETURN_ADD_KEY; + continue; + } + } + r = rule_add_key(&rule_tmp, TK_M_IMPORT_PROG, op, value, NULL); + } else if (streq(attr, "builtin")) { + const enum udev_builtin_cmd cmd = udev_builtin_lookup(value); + + if (cmd < 0) { + LOG_RULE_WARNING("IMPORT{builtin} '%s' unknown, ignoring", value); + continue; + } else + r = rule_add_key(&rule_tmp, TK_M_IMPORT_BUILTIN, op, value, &cmd); + } else if (streq(attr, "file")) + r = rule_add_key(&rule_tmp, TK_M_IMPORT_FILE, op, value, NULL); + else if (streq(attr, "db")) + r = rule_add_key(&rule_tmp, TK_M_IMPORT_DB, op, value, NULL); + else if (streq(attr, "cmdline")) + r = rule_add_key(&rule_tmp, TK_M_IMPORT_CMDLINE, op, value, NULL); + else if (streq(attr, "parent")) + r = rule_add_key(&rule_tmp, TK_M_IMPORT_PARENT, op, value, NULL); + else { + LOG_RULE_ERROR("Ignoring unknown %s{} type '%s'", "IMPORT", attr); + continue; + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "TEST")) { + mode_t mode = 0; + + if (op > OP_MATCH_MAX) + LOG_AND_RETURN("Invalid %s operation", "TEST"); + + attr = get_key_attribute(key + STRLEN("TEST")); + if (attr) { + mode = strtol(attr, NULL, 8); + r = rule_add_key(&rule_tmp, TK_M_TEST, op, value, &mode); + } else + r = rule_add_key(&rule_tmp, TK_M_TEST, op, value, NULL); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "RUN")) { + attr = get_key_attribute(key + STRLEN("RUN")); + if (!attr) + attr = "program"; + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", "RUN"); + + if (streq(attr, "builtin")) { + const enum udev_builtin_cmd cmd = udev_builtin_lookup(value); + + if (cmd < 0) { + LOG_RULE_ERROR("RUN{builtin}: '%s' unknown, ignoring", value); + continue; + } else + r = rule_add_key(&rule_tmp, TK_A_RUN_BUILTIN, op, value, &cmd); + } else if (streq(attr, "program")) { + const enum udev_builtin_cmd cmd = _UDEV_BUILTIN_MAX; + + r = rule_add_key(&rule_tmp, TK_A_RUN_PROGRAM, op, value, &cmd); + } else { + LOG_RULE_ERROR("Ignoring unknown %s{} type '%s'", "RUN", attr); + continue; + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (streq(key, "LABEL")) { + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + rule_tmp.rule.rule.label_off = rules_add_string(rules, value); + + } else if (streq(key, "GOTO")) { + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + if (rule_add_key(&rule_tmp, TK_A_GOTO, 0, value, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + } else if (startswith(key, "NAME")) { + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + if (op < OP_MATCH_MAX) + r = rule_add_key(&rule_tmp, TK_M_NAME, op, value, NULL); + else { + if (streq(value, "%k")) { + LOG_RULE_WARNING("NAME=\"%%k\" is ignored, because it breaks kernel supplied names; please remove"); + continue; + } + if (isempty(value)) { + LOG_RULE_DEBUG("NAME=\"\" is ignored, because udev will not delete any device nodes; please remove"); + continue; + } + r = rule_add_key(&rule_tmp, TK_A_NAME, op, value, NULL); + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + rule_tmp.rule.rule.can_set_name = true; + + } else if (streq(key, "SYMLINK")) { + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + if (op < OP_MATCH_MAX) + r = rule_add_key(&rule_tmp, TK_M_DEVLINK, op, value, NULL); + else + r = rule_add_key(&rule_tmp, TK_A_DEVLINK, op, value, NULL); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + rule_tmp.rule.rule.can_set_name = true; + + } else if (streq(key, "OWNER")) { + uid_t uid; + char *endptr; + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + uid = strtoul(value, &endptr, 10); + if (endptr[0] == '\0') + r = rule_add_key(&rule_tmp, TK_A_OWNER_ID, op, NULL, &uid); + else if (rules->resolve_name_timing == RESOLVE_NAME_EARLY && !strchr("$%", value[0])) { + uid = add_uid(rules, value); + r = rule_add_key(&rule_tmp, TK_A_OWNER_ID, op, NULL, &uid); + } else if (rules->resolve_name_timing != RESOLVE_NAME_NEVER) + r = rule_add_key(&rule_tmp, TK_A_OWNER, op, value, NULL); + else { + LOG_RULE_ERROR("Invalid %s operation", key); + continue; + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + rule_tmp.rule.rule.can_set_name = true; + + } else if (streq(key, "GROUP")) { + gid_t gid; + char *endptr; + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + gid = strtoul(value, &endptr, 10); + if (endptr[0] == '\0') + r = rule_add_key(&rule_tmp, TK_A_GROUP_ID, op, NULL, &gid); + else if ((rules->resolve_name_timing == RESOLVE_NAME_EARLY) && !strchr("$%", value[0])) { + gid = add_gid(rules, value); + r = rule_add_key(&rule_tmp, TK_A_GROUP_ID, op, NULL, &gid); + } else if (rules->resolve_name_timing != RESOLVE_NAME_NEVER) + r = rule_add_key(&rule_tmp, TK_A_GROUP, op, value, NULL); + else { + LOG_RULE_ERROR("Invalid %s operation", key); + continue; + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + rule_tmp.rule.rule.can_set_name = true; + + } else if (streq(key, "MODE")) { + mode_t mode; + char *endptr; + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + mode = strtol(value, &endptr, 8); + if (endptr[0] == '\0') + r = rule_add_key(&rule_tmp, TK_A_MODE_ID, op, NULL, &mode); + else + r = rule_add_key(&rule_tmp, TK_A_MODE, op, value, NULL); + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + + rule_tmp.rule.rule.can_set_name = true; + + } else if (streq(key, "OPTIONS")) { + const char *pos; + + if (op == OP_REMOVE) + LOG_AND_RETURN("Invalid %s operation", key); + + pos = strstr(value, "link_priority="); + if (pos) { + int prio = atoi(pos + STRLEN("link_priority=")); + + if (rule_add_key(&rule_tmp, TK_A_DEVLINK_PRIO, op, NULL, &prio) < 0) + LOG_AND_RETURN_ADD_KEY; + } + + pos = strstr(value, "string_escape="); + if (pos) { + pos += STRLEN("string_escape="); + if (startswith(pos, "none")) + r = rule_add_key(&rule_tmp, TK_A_STRING_ESCAPE_NONE, op, NULL, NULL); + else if (startswith(pos, "replace")) + r = rule_add_key(&rule_tmp, TK_A_STRING_ESCAPE_REPLACE, op, NULL, NULL); + else { + LOG_RULE_ERROR("OPTIONS: unknown string_escape mode '%s', ignoring", pos); + r = 0; + } + if (r < 0) + LOG_AND_RETURN_ADD_KEY; + } + + pos = strstr(value, "db_persist"); + if (pos) + if (rule_add_key(&rule_tmp, TK_A_DB_PERSIST, op, NULL, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + + pos = strstr(value, "nowatch"); + if (pos) { + static const int zero = 0; + if (rule_add_key(&rule_tmp, TK_A_INOTIFY_WATCH, op, NULL, &zero) < 0) + LOG_AND_RETURN_ADD_KEY; + } else { + static const int one = 1; + pos = strstr(value, "watch"); + if (pos) + if (rule_add_key(&rule_tmp, TK_A_INOTIFY_WATCH, op, NULL, &one) < 0) + LOG_AND_RETURN_ADD_KEY; + } + + pos = strstr(value, "static_node="); + if (pos) { + pos += STRLEN("static_node="); + if (rule_add_key(&rule_tmp, TK_A_STATIC_NODE, op, pos, NULL) < 0) + LOG_AND_RETURN_ADD_KEY; + rule_tmp.rule.rule.has_static_node = true; + } + + } else + LOG_AND_RETURN("Unknown key '%s'", key); + } + + /* add rule token and sort tokens */ + rule_tmp.rule.rule.token_count = 1 + rule_tmp.token_cur; + if (add_token(rules, &rule_tmp.rule) != 0 || sort_token(rules, &rule_tmp) != 0) + LOG_RULE_ERROR("Failed to add rule token"); +} + +static int parse_file(UdevRules *rules, const char *filename) { + _cleanup_fclose_ FILE *f = NULL; + unsigned first_token; + unsigned filename_off; + char line[UTIL_LINE_SIZE]; + int line_nr = 0; + unsigned i; + + f = fopen(filename, "re"); + if (!f) { + if (errno == ENOENT) + return 0; + + return -errno; + } + + if (null_or_empty_fd(fileno(f))) { + log_debug("Skipping empty file: %s", filename); + return 0; + } else + log_debug("Reading rules file: %s", filename); + + first_token = rules->token_cur; + filename_off = rules_add_string(rules, filename); + + while (fgets(line, sizeof(line), f)) { + char *key; + size_t len; + + /* skip whitespace */ + line_nr++; + key = line; + while (isspace(key[0])) + key++; + + /* comment */ + if (key[0] == '#') + continue; + + len = strlen(line); + if (len < 3) + continue; + + /* continue reading if backslash+newline is found */ + while (line[len-2] == '\\') { + if (!fgets(&line[len-2], (sizeof(line)-len)+2, f)) + break; + if (strlen(&line[len-2]) < 2) + break; + line_nr++; + len = strlen(line); + } + + if (len+1 >= sizeof(line)) { + log_error("Line too long '%s':%u, ignored", filename, line_nr); + continue; + } + add_rule(rules, key, filename, filename_off, line_nr); + } + + /* link GOTOs to LABEL rules in this file to be able to fast-forward */ + for (i = first_token+1; i < rules->token_cur; i++) { + if (rules->tokens[i].type == TK_A_GOTO) { + char *label = rules_str(rules, rules->tokens[i].key.value_off); + unsigned j; + + for (j = i+1; j < rules->token_cur; j++) { + if (rules->tokens[j].type != TK_RULE) + continue; + if (rules->tokens[j].rule.label_off == 0) + continue; + if (!streq(label, rules_str(rules, rules->tokens[j].rule.label_off))) + continue; + rules->tokens[i].key.rule_goto = j; + break; + } + if (rules->tokens[i].key.rule_goto == 0) + log_error("GOTO '%s' has no matching label in: '%s'", label, filename); + } + } + return 0; +} + +int udev_rules_new(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + _cleanup_strv_free_ char **files = NULL; + char **f; + int r; + + assert(resolve_name_timing >= 0 && resolve_name_timing < _RESOLVE_NAME_TIMING_MAX); + + rules = new(UdevRules, 1); + if (!rules) + return -ENOMEM; + + *rules = (UdevRules) { + .resolve_name_timing = resolve_name_timing, + }; + + /* init token array and string buffer */ + rules->tokens = malloc_multiply(PREALLOC_TOKEN, sizeof(struct token)); + if (!rules->tokens) + return -ENOMEM; + rules->token_max = PREALLOC_TOKEN; + + rules->strbuf = strbuf_new(); + if (!rules->strbuf) + return -ENOMEM; + + udev_rules_check_timestamp(rules); + + r = conf_files_list_strv(&files, ".rules", NULL, 0, rules_dirs); + if (r < 0) + return log_error_errno(r, "Failed to enumerate rules files: %m"); + + /* + * The offset value in the rules strct is limited; add all + * rules file names to the beginning of the string buffer. + */ + STRV_FOREACH(f, files) + rules_add_string(rules, *f); + + STRV_FOREACH(f, files) + parse_file(rules, *f); + + struct token end_token = { .type = TK_END }; + add_token(rules, &end_token); + log_debug("Rules contain %zu bytes tokens (%u * %zu bytes), %zu bytes strings", + rules->token_max * sizeof(struct token), rules->token_max, sizeof(struct token), rules->strbuf->len); + + /* cleanup temporary strbuf data */ + log_debug("%zu strings (%zu bytes), %zu de-duplicated (%zu bytes), %zu trie nodes used", + rules->strbuf->in_count, rules->strbuf->in_len, + rules->strbuf->dedup_count, rules->strbuf->dedup_len, rules->strbuf->nodes_count); + strbuf_complete(rules->strbuf); + + /* cleanup uid/gid cache */ + rules->uids = mfree(rules->uids); + rules->uids_cur = 0; + rules->uids_max = 0; + rules->gids = mfree(rules->gids); + rules->gids_cur = 0; + rules->gids_max = 0; + + dump_rules(rules); + *ret_rules = TAKE_PTR(rules); + return 0; +} + +UdevRules *udev_rules_free(UdevRules *rules) { + if (!rules) + return NULL; + free(rules->tokens); + strbuf_cleanup(rules->strbuf); + free(rules->uids); + free(rules->gids); + return mfree(rules); +} + +bool udev_rules_check_timestamp(UdevRules *rules) { + if (!rules) + return false; + + return paths_check_timestamp(rules_dirs, &rules->dirs_ts_usec, true); +} + +static int match_key(UdevRules *rules, struct token *token, const char *val) { + char *key_value = rules_str(rules, token->key.value_off); + char *pos; + bool match = false; + + if (!val) + val = ""; + + switch (token->key.glob) { + case GL_PLAIN: + match = (streq(key_value, val)); + break; + case GL_GLOB: + match = (fnmatch(key_value, val, 0) == 0); + break; + case GL_SPLIT: + { + const char *s; + size_t len; + + s = rules_str(rules, token->key.value_off); + len = strlen(val); + for (;;) { + const char *next; + + next = strchr(s, '|'); + if (next) { + size_t matchlen = (size_t)(next - s); + + match = (matchlen == len && strneq(s, val, matchlen)); + if (match) + break; + } else { + match = (streq(s, val)); + break; + } + s = &next[1]; + } + break; + } + case GL_SPLIT_GLOB: + { + char value[UTIL_PATH_SIZE]; + + strscpy(value, sizeof(value), rules_str(rules, token->key.value_off)); + key_value = value; + while (key_value) { + pos = strchr(key_value, '|'); + if (pos) { + pos[0] = '\0'; + pos = &pos[1]; + } + match = (fnmatch(key_value, val, 0) == 0); + if (match) + break; + key_value = pos; + } + break; + } + case GL_SOMETHING: + match = (val[0] != '\0'); + break; + case GL_UNSET: + return -1; + } + + if (match && (token->key.op == OP_MATCH)) + return 0; + if (!match && (token->key.op == OP_NOMATCH)) + return 0; + return -1; +} + +static int match_attr(UdevRules *rules, sd_device *dev, UdevEvent *event, struct token *cur) { + char nbuf[UTIL_NAME_SIZE], vbuf[UTIL_NAME_SIZE]; + const char *name, *value; + size_t len; + + name = rules_str(rules, cur->key.attr_off); + switch (cur->key.attrsubst) { + case SB_FORMAT: + udev_event_apply_format(event, name, nbuf, sizeof(nbuf), false); + name = nbuf; + _fallthrough_; + case SB_NONE: + if (sd_device_get_sysattr_value(dev, name, &value) < 0) + return -1; + break; + case SB_SUBSYS: + if (util_resolve_subsys_kernel(name, vbuf, sizeof(vbuf), true) != 0) + return -1; + value = vbuf; + break; + default: + return -1; + } + + /* remove trailing whitespace, if not asked to match for it */ + len = strlen(value); + if (len > 0 && isspace(value[len-1])) { + const char *key_value; + size_t klen; + + key_value = rules_str(rules, cur->key.value_off); + klen = strlen(key_value); + if (klen > 0 && !isspace(key_value[klen-1])) { + if (value != vbuf) { + strscpy(vbuf, sizeof(vbuf), value); + value = vbuf; + } + while (len > 0 && isspace(vbuf[--len])) + vbuf[len] = '\0'; + } + } + + return match_key(rules, cur, value); +} + +enum escape_type { + ESCAPE_UNSET, + ESCAPE_NONE, + ESCAPE_REPLACE, +}; + +int udev_rules_apply_to_event( + UdevRules *rules, + UdevEvent *event, + usec_t timeout_usec, + Hashmap *properties_list) { + sd_device *dev = event->dev; + enum escape_type esc = ESCAPE_UNSET; + struct token *cur, *rule; + const char *action, *val; + bool can_set_name; + int r; + + if (!rules->tokens) + return 0; + + r = sd_device_get_property_value(dev, "ACTION", &action); + if (r < 0) + return r; + + can_set_name = (!streq(action, "remove") && + (sd_device_get_devnum(dev, NULL) >= 0 || + sd_device_get_ifindex(dev, NULL) >= 0)); + + /* loop through token list, match, run actions or forward to next rule */ + cur = &rules->tokens[0]; + rule = cur; + for (;;) { + dump_token(rules, cur); + switch (cur->type) { + case TK_RULE: + /* current rule */ + rule = cur; + /* possibly skip rules which want to set NAME, SYMLINK, OWNER, GROUP, MODE */ + if (!can_set_name && rule->rule.can_set_name) + goto nomatch; + esc = ESCAPE_UNSET; + break; + case TK_M_ACTION: + if (match_key(rules, cur, action) != 0) + goto nomatch; + break; + case TK_M_DEVPATH: + if (sd_device_get_devpath(dev, &val) < 0) + goto nomatch; + if (match_key(rules, cur, val) != 0) + goto nomatch; + break; + case TK_M_KERNEL: + if (sd_device_get_sysname(dev, &val) < 0) + goto nomatch; + if (match_key(rules, cur, val) != 0) + goto nomatch; + break; + case TK_M_DEVLINK: { + const char *devlink; + bool match = false; + + FOREACH_DEVICE_DEVLINK(dev, devlink) + if (match_key(rules, cur, devlink + STRLEN("/dev/")) == 0) { + match = true; + break; + } + + if (!match) + goto nomatch; + break; + } + case TK_M_NAME: + if (match_key(rules, cur, event->name) != 0) + goto nomatch; + break; + case TK_M_ENV: { + const char *key_name = rules_str(rules, cur->key.attr_off); + + if (sd_device_get_property_value(dev, key_name, &val) < 0) { + /* check global properties */ + if (properties_list) + val = hashmap_get(properties_list, key_name); + else + val = NULL; + } + + if (match_key(rules, cur, strempty(val))) + goto nomatch; + break; + } + case TK_M_TAG: { + bool match = false; + const char *tag; + + FOREACH_DEVICE_TAG(dev, tag) + if (streq(rules_str(rules, cur->key.value_off), tag)) { + match = true; + break; + } + + if ((!match && (cur->key.op != OP_NOMATCH)) || + (match && (cur->key.op == OP_NOMATCH))) + goto nomatch; + break; + } + case TK_M_SUBSYSTEM: + if (sd_device_get_subsystem(dev, &val) < 0) + goto nomatch; + if (match_key(rules, cur, val) != 0) + goto nomatch; + break; + case TK_M_DRIVER: + if (sd_device_get_driver(dev, &val) < 0) + goto nomatch; + if (match_key(rules, cur, val) != 0) + goto nomatch; + break; + case TK_M_ATTR: + if (match_attr(rules, dev, event, cur) != 0) + goto nomatch; + break; + case TK_M_SYSCTL: { + char filename[UTIL_PATH_SIZE]; + _cleanup_free_ char *value = NULL; + size_t len; + + udev_event_apply_format(event, rules_str(rules, cur->key.attr_off), filename, sizeof(filename), false); + sysctl_normalize(filename); + if (sysctl_read(filename, &value) < 0) + goto nomatch; + + len = strlen(value); + while (len > 0 && isspace(value[--len])) + value[len] = '\0'; + if (match_key(rules, cur, value) != 0) + goto nomatch; + break; + } + case TK_M_KERNELS: + case TK_M_SUBSYSTEMS: + case TK_M_DRIVERS: + case TK_M_ATTRS: + case TK_M_TAGS: { + struct token *next; + + /* get whole sequence of parent matches */ + next = cur; + while (next->type > TK_M_PARENTS_MIN && next->type < TK_M_PARENTS_MAX) + next++; + + /* loop over parents */ + event->dev_parent = dev; + for (;;) { + struct token *key; + + /* loop over sequence of parent match keys */ + for (key = cur; key < next; key++ ) { + dump_token(rules, key); + switch(key->type) { + case TK_M_KERNELS: + if (sd_device_get_sysname(event->dev_parent, &val) < 0) + goto try_parent; + if (match_key(rules, key, val) != 0) + goto try_parent; + break; + case TK_M_SUBSYSTEMS: + if (sd_device_get_subsystem(event->dev_parent, &val) < 0) + goto try_parent; + if (match_key(rules, key, val) != 0) + goto try_parent; + break; + case TK_M_DRIVERS: + if (sd_device_get_driver(event->dev_parent, &val) < 0) + goto try_parent; + if (match_key(rules, key, val) != 0) + goto try_parent; + break; + case TK_M_ATTRS: + if (match_attr(rules, event->dev_parent, event, key) != 0) + goto try_parent; + break; + case TK_M_TAGS: { + bool match = sd_device_has_tag(event->dev_parent, rules_str(rules, cur->key.value_off)); + + if (match && key->key.op == OP_NOMATCH) + goto try_parent; + if (!match && key->key.op == OP_MATCH) + goto try_parent; + break; + } + default: + goto nomatch; + } + } + break; + + try_parent: + if (sd_device_get_parent(event->dev_parent, &event->dev_parent) < 0) { + event->dev_parent = NULL; + goto nomatch; + } + } + /* move behind our sequence of parent match keys */ + cur = next; + continue; + } + case TK_M_TEST: { + char filename[UTIL_PATH_SIZE]; + struct stat statbuf; + int match; + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), filename, sizeof(filename), false); + if (util_resolve_subsys_kernel(filename, filename, sizeof(filename), false) != 0) { + if (filename[0] != '/') { + char tmp[UTIL_PATH_SIZE]; + + if (sd_device_get_syspath(dev, &val) < 0) + goto nomatch; + + strscpy(tmp, sizeof(tmp), filename); + strscpyl(filename, sizeof(filename), val, "/", tmp, NULL); + } + } + attr_subst_subdir(filename, sizeof(filename)); + + match = (stat(filename, &statbuf) == 0); + if (match && cur->key.mode > 0) + match = ((statbuf.st_mode & cur->key.mode) > 0); + if (match && cur->key.op == OP_NOMATCH) + goto nomatch; + if (!match && cur->key.op == OP_MATCH) + goto nomatch; + break; + } + case TK_M_PROGRAM: { + char program[UTIL_PATH_SIZE], result[UTIL_LINE_SIZE]; + + event->program_result = mfree(event->program_result); + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), program, sizeof(program), false); + log_device_debug(dev, "PROGRAM '%s' %s:%u", + program, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + + if (udev_event_spawn(event, timeout_usec, true, program, result, sizeof(result)) != 0) { + if (cur->key.op != OP_NOMATCH) + goto nomatch; + } else { + int count; + + delete_trailing_chars(result, "\n"); + if (IN_SET(esc, ESCAPE_UNSET, ESCAPE_REPLACE)) { + count = util_replace_chars(result, UDEV_ALLOWED_CHARS_INPUT); + if (count > 0) + log_device_debug(dev, "Replaced %i character(s) from result of '%s'" , count, program); + } + event->program_result = strdup(result); + if (cur->key.op == OP_NOMATCH) + goto nomatch; + } + break; + } + case TK_M_IMPORT_FILE: { + char import[UTIL_PATH_SIZE]; + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), import, sizeof(import), false); + if (import_file_into_properties(dev, import) != 0) + if (cur->key.op != OP_NOMATCH) + goto nomatch; + break; + } + case TK_M_IMPORT_PROG: { + char import[UTIL_PATH_SIZE]; + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), import, sizeof(import), false); + log_device_debug(dev, "IMPORT '%s' %s:%u", + import, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + + if (import_program_into_properties(event, timeout_usec, import) != 0) + if (cur->key.op != OP_NOMATCH) + goto nomatch; + break; + } + case TK_M_IMPORT_BUILTIN: { + char command[UTIL_PATH_SIZE]; + + if (udev_builtin_run_once(cur->key.builtin_cmd)) { + /* check if we ran already */ + if (event->builtin_run & (1 << cur->key.builtin_cmd)) { + log_device_debug(dev, "IMPORT builtin skip '%s' %s:%u", + udev_builtin_name(cur->key.builtin_cmd), + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + /* return the result from earlier run */ + if (event->builtin_ret & (1 << cur->key.builtin_cmd)) + if (cur->key.op != OP_NOMATCH) + goto nomatch; + break; + } + /* mark as ran */ + event->builtin_run |= (1 << cur->key.builtin_cmd); + } + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), command, sizeof(command), false); + log_device_debug(dev, "IMPORT builtin '%s' %s:%u", + udev_builtin_name(cur->key.builtin_cmd), + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + + r = udev_builtin_run(dev, cur->key.builtin_cmd, command, false); + if (r < 0) { + /* remember failure */ + log_device_debug_errno(dev, r, "IMPORT builtin '%s' fails: %m", + udev_builtin_name(cur->key.builtin_cmd)); + event->builtin_ret |= (1 << cur->key.builtin_cmd); + if (cur->key.op != OP_NOMATCH) + goto nomatch; + } + break; + } + case TK_M_IMPORT_DB: { + const char *key; + + key = rules_str(rules, cur->key.value_off); + if (event->dev_db_clone && + sd_device_get_property_value(event->dev_db_clone, key, &val) >= 0) + device_add_property(dev, key, val); + else if (cur->key.op != OP_NOMATCH) + goto nomatch; + break; + } + case TK_M_IMPORT_CMDLINE: { + _cleanup_free_ char *value = NULL; + bool imported = false; + const char *key; + + key = rules_str(rules, cur->key.value_off); + r = proc_cmdline_get_key(key, PROC_CMDLINE_VALUE_OPTIONAL, &value); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to read %s from /proc/cmdline, ignoring: %m", key); + else if (r > 0) { + imported = true; + + if (value) + device_add_property(dev, key, value); + else + /* we import simple flags as 'FLAG=1' */ + device_add_property(dev, key, "1"); + } + + if (!imported && cur->key.op != OP_NOMATCH) + goto nomatch; + break; + } + case TK_M_IMPORT_PARENT: { + char import[UTIL_PATH_SIZE]; + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), import, sizeof(import), false); + if (import_parent_into_properties(dev, import) != 0) + if (cur->key.op != OP_NOMATCH) + goto nomatch; + break; + } + case TK_M_RESULT: + if (match_key(rules, cur, event->program_result) != 0) + goto nomatch; + break; + case TK_A_STRING_ESCAPE_NONE: + esc = ESCAPE_NONE; + break; + case TK_A_STRING_ESCAPE_REPLACE: + esc = ESCAPE_REPLACE; + break; + case TK_A_DB_PERSIST: + device_set_db_persist(dev); + break; + case TK_A_INOTIFY_WATCH: + if (event->inotify_watch_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->inotify_watch_final = true; + event->inotify_watch = cur->key.watch; + break; + case TK_A_DEVLINK_PRIO: + device_set_devlink_priority(dev, cur->key.devlink_prio); + break; + case TK_A_OWNER: { + char owner[UTIL_NAME_SIZE]; + const char *ow = owner; + + if (event->owner_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->owner_final = true; + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), owner, sizeof(owner), false); + event->owner_set = true; + r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING); + if (r < 0) { + log_unknown_owner(dev, r, "user", owner); + event->uid = 0; + } + log_device_debug(dev, "OWNER %u %s:%u", + event->uid, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + } + case TK_A_GROUP: { + char group[UTIL_NAME_SIZE]; + const char *gr = group; + + if (event->group_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->group_final = true; + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), group, sizeof(group), false); + event->group_set = true; + r = get_group_creds(&gr, &event->gid, USER_CREDS_ALLOW_MISSING); + if (r < 0) { + log_unknown_owner(dev, r, "group", group); + event->gid = 0; + } + log_device_debug(dev, "GROUP %u %s:%u", + event->gid, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + } + case TK_A_MODE: { + char mode_str[UTIL_NAME_SIZE]; + mode_t mode; + + if (event->mode_final) + break; + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), mode_str, sizeof(mode_str), false); + r = parse_mode(mode_str, &mode); + if (r < 0) { + log_device_error_errno(dev, r, "Failed to parse mode '%s': %m", mode_str); + break; + } + if (cur->key.op == OP_ASSIGN_FINAL) + event->mode_final = true; + event->mode_set = true; + event->mode = mode; + log_device_debug(dev, "MODE %#o %s:%u", + event->mode, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + } + case TK_A_OWNER_ID: + if (event->owner_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->owner_final = true; + event->owner_set = true; + event->uid = cur->key.uid; + log_device_debug(dev, "OWNER %u %s:%u", + event->uid, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + case TK_A_GROUP_ID: + if (event->group_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->group_final = true; + event->group_set = true; + event->gid = cur->key.gid; + log_device_debug(dev, "GROUP %u %s:%u", + event->gid, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + case TK_A_MODE_ID: + if (event->mode_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->mode_final = true; + event->mode_set = true; + event->mode = cur->key.mode; + log_device_debug(dev, "MODE %#o %s:%u", + event->mode, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + case TK_A_SECLABEL: { + _cleanup_free_ char *name = NULL, *label = NULL; + char label_str[UTIL_LINE_SIZE] = {}; + + name = strdup(rules_str(rules, cur->key.attr_off)); + if (!name) + return log_oom(); + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), label_str, sizeof(label_str), false); + if (!isempty(label_str)) + label = strdup(label_str); + else + label = strdup(rules_str(rules, cur->key.value_off)); + if (!label) + return log_oom(); + + if (IN_SET(cur->key.op, OP_ASSIGN, OP_ASSIGN_FINAL)) + hashmap_clear_free_free(event->seclabel_list); + + r = hashmap_ensure_allocated(&event->seclabel_list, NULL); + if (r < 0) + return log_oom(); + + r = hashmap_put(event->seclabel_list, name, label); + if (r < 0) + return log_oom(); + log_device_debug(dev, "SECLABEL{%s}='%s' %s:%u", + name, label, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + name = label = NULL; + + break; + } + case TK_A_ENV: { + char value_new[UTIL_NAME_SIZE]; + const char *name, *value_old; + + name = rules_str(rules, cur->key.attr_off); + val = rules_str(rules, cur->key.value_off); + if (val[0] == '\0') { + if (cur->key.op == OP_ADD) + break; + device_add_property(dev, name, NULL); + break; + } + + if (cur->key.op == OP_ADD && + sd_device_get_property_value(dev, name, &value_old) >= 0) { + char temp[UTIL_NAME_SIZE]; + + /* append value separated by space */ + udev_event_apply_format(event, val, temp, sizeof(temp), false); + strscpyl(value_new, sizeof(value_new), value_old, " ", temp, NULL); + } else + udev_event_apply_format(event, val, value_new, sizeof(value_new), false); + + device_add_property(dev, name, value_new); + break; + } + case TK_A_TAG: { + char tag[UTIL_PATH_SIZE]; + const char *p; + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), tag, sizeof(tag), false); + if (IN_SET(cur->key.op, OP_ASSIGN, OP_ASSIGN_FINAL)) + device_cleanup_tags(dev); + for (p = tag; *p != '\0'; p++) { + if ((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || + IN_SET(*p, '-', '_')) + continue; + log_device_error(dev, "Ignoring invalid tag name '%s'", tag); + break; + } + if (cur->key.op == OP_REMOVE) + device_remove_tag(dev, tag); + else + device_add_tag(dev, tag); + break; + } + case TK_A_NAME: { + char name_str[UTIL_PATH_SIZE]; + const char *name; + int count; + + name = rules_str(rules, cur->key.value_off); + if (event->name_final) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->name_final = true; + udev_event_apply_format(event, name, name_str, sizeof(name_str), false); + if (IN_SET(esc, ESCAPE_UNSET, ESCAPE_REPLACE)) { + count = util_replace_chars(name_str, "/"); + if (count > 0) + log_device_debug(dev, "Replaced %i character(s) from result of NAME=\"%s\"", count, name); + } + if (sd_device_get_devnum(dev, NULL) >= 0 && + (sd_device_get_devname(dev, &val) < 0 || + !streq(name_str, val + STRLEN("/dev/")))) { + log_device_error(dev, "Kernel device nodes cannot be renamed, ignoring NAME=\"%s\"; please fix it in %s:%u\n", + name, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + } + if (free_and_strdup(&event->name, name_str) < 0) + return log_oom(); + + log_device_debug(dev, "NAME '%s' %s:%u", + event->name, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + } + case TK_A_DEVLINK: { + char temp[UTIL_PATH_SIZE], filename[UTIL_PATH_SIZE], *pos, *next; + int count = 0; + + if (event->devlink_final) + break; + if (sd_device_get_devnum(dev, NULL) < 0) + break; + if (cur->key.op == OP_ASSIGN_FINAL) + event->devlink_final = true; + if (IN_SET(cur->key.op, OP_ASSIGN, OP_ASSIGN_FINAL)) + device_cleanup_devlinks(dev); + + /* allow multiple symlinks separated by spaces */ + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), temp, sizeof(temp), esc != ESCAPE_NONE); + if (esc == ESCAPE_UNSET) + count = util_replace_chars(temp, "/ "); + else if (esc == ESCAPE_REPLACE) + count = util_replace_chars(temp, "/"); + if (count > 0) + log_device_debug(dev, "Replaced %i character(s) from result of LINK" , count); + pos = temp; + while (isspace(pos[0])) + pos++; + next = strchr(pos, ' '); + while (next) { + next[0] = '\0'; + log_device_debug(dev, "LINK '%s' %s:%u", pos, + rules_str(rules, rule->rule.filename_off), rule->rule.filename_line); + strscpyl(filename, sizeof(filename), "/dev/", pos, NULL); + device_add_devlink(dev, filename); + while (isspace(next[1])) + next++; + pos = &next[1]; + next = strchr(pos, ' '); + } + if (pos[0] != '\0') { + log_device_debug(dev, "LINK '%s' %s:%u", pos, + rules_str(rules, rule->rule.filename_off), rule->rule.filename_line); + strscpyl(filename, sizeof(filename), "/dev/", pos, NULL); + device_add_devlink(dev, filename); + } + break; + } + case TK_A_ATTR: { + char attr[UTIL_PATH_SIZE], value[UTIL_NAME_SIZE]; + _cleanup_fclose_ FILE *f = NULL; + const char *key_name; + + key_name = rules_str(rules, cur->key.attr_off); + if (util_resolve_subsys_kernel(key_name, attr, sizeof(attr), false) != 0 && + sd_device_get_syspath(dev, &val) >= 0) + strscpyl(attr, sizeof(attr), val, "/", key_name, NULL); + attr_subst_subdir(attr, sizeof(attr)); + + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), value, sizeof(value), false); + log_device_debug(dev, "ATTR '%s' writing '%s' %s:%u", attr, value, + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + f = fopen(attr, "we"); + if (!f) + log_device_error_errno(dev, errno, "Failed to open ATTR{%s} for writing: %m", attr); + else if (fprintf(f, "%s", value) <= 0) + log_device_error_errno(dev, errno, "Failed to write ATTR{%s}: %m", attr); + break; + } + case TK_A_SYSCTL: { + char filename[UTIL_PATH_SIZE], value[UTIL_NAME_SIZE]; + + udev_event_apply_format(event, rules_str(rules, cur->key.attr_off), filename, sizeof(filename), false); + sysctl_normalize(filename); + udev_event_apply_format(event, rules_str(rules, cur->key.value_off), value, sizeof(value), false); + log_device_debug(dev, "SYSCTL '%s' writing '%s' %s:%u", filename, value, + rules_str(rules, rule->rule.filename_off), rule->rule.filename_line); + r = sysctl_write(filename, value); + if (r < 0) + log_device_error_errno(dev, r, "Failed to write SYSCTL{%s}='%s': %m", filename, value); + break; + } + case TK_A_RUN_BUILTIN: + case TK_A_RUN_PROGRAM: { + _cleanup_free_ char *cmd = NULL; + + if (IN_SET(cur->key.op, OP_ASSIGN, OP_ASSIGN_FINAL)) + hashmap_clear_free_key(event->run_list); + + r = hashmap_ensure_allocated(&event->run_list, NULL); + if (r < 0) + return log_oom(); + + cmd = strdup(rules_str(rules, cur->key.value_off)); + if (!cmd) + return log_oom(); + + r = hashmap_put(event->run_list, cmd, INT_TO_PTR(cur->key.builtin_cmd)); + if (r < 0) + return log_oom(); + + cmd = NULL; + + log_device_debug(dev, "RUN '%s' %s:%u", + rules_str(rules, cur->key.value_off), + rules_str(rules, rule->rule.filename_off), + rule->rule.filename_line); + break; + } + case TK_A_GOTO: + if (cur->key.rule_goto == 0) + break; + cur = &rules->tokens[cur->key.rule_goto]; + continue; + case TK_END: + return 0; + + case TK_M_PARENTS_MIN: + case TK_M_PARENTS_MAX: + case TK_M_MAX: + case TK_UNSET: + log_device_error(dev, "Wrong type %u", cur->type); + goto nomatch; + } + + cur++; + continue; + nomatch: + /* fast-forward to next rule */ + cur = rule + rule->rule.token_count; + } + + return 0; +} + +int udev_rules_apply_static_dev_perms(UdevRules *rules) { + struct token *cur; + struct token *rule; + uid_t uid = 0; + gid_t gid = 0; + mode_t mode = 0; + _cleanup_strv_free_ char **tags = NULL; + char **t; + FILE *f = NULL; + _cleanup_free_ char *path = NULL; + int r; + + if (!rules->tokens) + return 0; + + cur = &rules->tokens[0]; + rule = cur; + for (;;) { + switch (cur->type) { + case TK_RULE: + /* current rule */ + rule = cur; + + /* skip rules without a static_node tag */ + if (!rule->rule.has_static_node) + goto next; + + uid = 0; + gid = 0; + mode = 0; + tags = strv_free(tags); + break; + case TK_A_OWNER_ID: + uid = cur->key.uid; + break; + case TK_A_GROUP_ID: + gid = cur->key.gid; + break; + case TK_A_MODE_ID: + mode = cur->key.mode; + break; + case TK_A_TAG: + r = strv_extend(&tags, rules_str(rules, cur->key.value_off)); + if (r < 0) + goto finish; + + break; + case TK_A_STATIC_NODE: { + char device_node[UTIL_PATH_SIZE]; + char tags_dir[UTIL_PATH_SIZE]; + char tag_symlink[UTIL_PATH_SIZE]; + struct stat stats; + + /* we assure, that the permissions tokens are sorted before the static token */ + + if (mode == 0 && uid == 0 && gid == 0 && !tags) + goto next; + + strscpyl(device_node, sizeof(device_node), "/dev/", rules_str(rules, cur->key.value_off), NULL); + if (stat(device_node, &stats) != 0) + break; + if (!S_ISBLK(stats.st_mode) && !S_ISCHR(stats.st_mode)) + break; + + /* export the tags to a directory as symlinks, allowing otherwise dead nodes to be tagged */ + if (tags) { + STRV_FOREACH(t, tags) { + _cleanup_free_ char *unescaped_filename = NULL; + + 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); + + unescaped_filename = xescape(rules_str(rules, cur->key.value_off), "/."); + + 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 (mode == 0 && uid == 0 && gid == 0) + break; + + if (mode == 0) { + if (gid > 0) + mode = 0660; + else + mode = 0600; + } + if (mode != (stats.st_mode & 01777)) { + r = chmod(device_node, mode); + if (r < 0) + return log_error_errno(errno, "Failed to chmod '%s' %#o: %m", + device_node, mode); + else + log_debug("chmod '%s' %#o", device_node, mode); + } + + if ((uid != 0 && uid != stats.st_uid) || (gid != 0 && gid != stats.st_gid)) { + r = chown(device_node, uid, gid); + if (r < 0) + return log_error_errno(errno, "Failed to chown '%s' %u %u: %m", + device_node, uid, gid); + else + log_debug("chown '%s' %u %u", device_node, uid, gid); + } + + utimensat(AT_FDCWD, device_node, NULL, 0); + break; + } + case TK_END: + goto finish; + } + + cur++; + continue; +next: + /* fast-forward to next rule */ + cur = rule + rule->rule.token_count; + continue; + } + +finish: + if (f) { + fflush(f); + fchmod(fileno(f), 0644); + if (ferror(f) || rename(path, "/run/udev/static_node-tags") < 0) { + unlink_noerrno("/run/udev/static_node-tags"); + unlink_noerrno(path); + return -errno; + } + } + + return 0; +} diff --git a/src/udev/udev-watch.c b/src/udev/udev-watch.c new file mode 100644 index 0000000..68b51d0 --- /dev/null +++ b/src/udev/udev-watch.c @@ -0,0 +1,177 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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(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 log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid inotify descriptor."); + + 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..24a136d --- /dev/null +++ b/src/udev/udev-watch.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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..7deb771 --- /dev/null +++ b/src/udev/udev.conf @@ -0,0 +1,10 @@ +# 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 +#resolve_names=early diff --git a/src/udev/udev.h b/src/udev/udev.h new file mode 100644 index 0000000..3bc69ff --- /dev/null +++ b/src/udev/udev.h @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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-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; + Hashmap *seclabel_list; + Hashmap *run_list; + usec_t exec_delay_usec; + usec_t birth_usec; + sd_netlink *rtnl; + unsigned builtin_run; + unsigned builtin_ret; + bool inotify_watch; + bool inotify_watch_final; + bool group_set; + bool group_final; + bool owner_set; + bool owner_final; + bool mode_set; + bool mode_final; + bool name_final; + bool devlink_final; + bool run_final; +} UdevEvent; + +/* udev-rules.c */ +typedef struct UdevRules UdevRules; + +int udev_rules_new(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing); +UdevRules *udev_rules_free(UdevRules *rules); + +bool udev_rules_check_timestamp(UdevRules *rules); +int udev_rules_apply_to_event(UdevRules *rules, UdevEvent *event, + usec_t timeout_usec, + Hashmap *properties_list); +int udev_rules_apply_static_dev_perms(UdevRules *rules); + +static inline usec_t udev_warn_timeout(usec_t timeout_usec) { + return DIV_ROUND_UP(timeout_usec, 3); +} + +/* udev-event.c */ +UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl); +UdevEvent *udev_event_free(UdevEvent *event); +ssize_t udev_event_apply_format(UdevEvent *event, + const char *src, char *dest, size_t size, + bool replace_whitespace); +int udev_event_spawn(UdevEvent *event, + usec_t timeout_usec, + bool accept_failure, + const char *cmd, char *result, size_t ressize); +int udev_event_execute_rules(UdevEvent *event, + usec_t timeout_usec, + Hashmap *properties_list, + UdevRules *rules); +void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec); + +/* Cleanup functions */ +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevEvent*, udev_event_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRules*, udev_rules_free); diff --git a/src/udev/udev.pc.in b/src/udev/udev.pc.in new file mode 100644 index 0000000..5acbb2d --- /dev/null +++ b/src/udev/udev.pc.in @@ -0,0 +1,5 @@ +Name: udev +Description: udev +Version: @PROJECT_VERSION@ + +udevdir=@udevlibexecdir@ diff --git a/src/udev/udevadm-control.c b/src/udev/udevadm-control.c new file mode 100644 index 0000000..7cfc4c9 --- /dev/null +++ b/src/udev/udevadm-control.c @@ -0,0 +1,169 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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-priority=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-priority", required_argument, NULL, 'l' }, + { "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' }, + {} + }; + + r = must_be_root(); + if (r < 0) + return r; + + 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."); + + uctrl = udev_ctrl_new(); + if (!uctrl) + return log_oom(); + + while ((c = getopt_long(argc, argv, "el:sSRp:m:t:Vh", options, NULL)) >= 0) + switch (c) { + case 'e': + r = udev_ctrl_send_exit(uctrl, timeout); + if (r < 0) + return r; + break; + case 'l': + r = log_level_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse log priority '%s': %m", optarg); + + r = udev_ctrl_send_set_log_level(uctrl, r, timeout); + if (r < 0) + return r; + break; + case 's': + r = udev_ctrl_send_stop_exec_queue(uctrl, timeout); + if (r < 0) + return r; + break; + case 'S': + r = udev_ctrl_send_start_exec_queue(uctrl, timeout); + if (r < 0) + return r; + break; + case 'R': + r = udev_ctrl_send_reload(uctrl, timeout); + if (r < 0) + return r; + 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, timeout); + if (r < 0) + return r; + 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, timeout); + if (r < 0) + return r; + break; + } + case ARG_PING: + r = udev_ctrl_send_ping(uctrl, timeout); + if (r < 0) + return log_error_errno(r, "Failed to connect to udev daemon: %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]); + + return 0; +} diff --git a/src/udev/udevadm-hwdb.c b/src/udev/udevadm-hwdb.c new file mode 100644 index 0000000..7f8960f --- /dev/null +++ b/src/udev/udevadm-hwdb.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#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..ebd15d3 --- /dev/null +++ b/src/udev/udevadm-info.c @@ -0,0 +1,470 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <string.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 "string-util.h" +#include "udevadm.h" +#include "udevadm-util.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 bool skip_attribute(const char *name) { + static const char* const skip[] = { + "uevent", + "dev", + "modalias", + "resource", + "driver", + "subsystem", + "module", + }; + unsigned i; + + for (i = 0; i < ELEMENTSOF(skip); i++) + if (streq(name, skip[i])) + return true; + return false; +} + +static void print_all_attributes(sd_device *device, const char *key) { + const char *name, *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(value[len-1])) + len--; + if (len > 0) + continue; + + printf(" %s{%s}==\"%s\"\n", key, name, value); + } + puts(""); +} + +static int print_device_chain(sd_device *device) { + sd_device *child, *parent; + const char *str; + + 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"); + + (void) sd_device_get_devpath(device, &str); + printf(" looking at device '%s':\n", str); + (void) sd_device_get_sysname(device, &str); + printf(" KERNEL==\"%s\"\n", str); + if (sd_device_get_subsystem(device, &str) < 0) + str = ""; + printf(" SUBSYSTEM==\"%s\"\n", str); + if (sd_device_get_driver(device, &str) < 0) + str = ""; + printf(" DRIVER==\"%s\"\n", str); + print_all_attributes(device, "ATTR"); + + for (child = device; sd_device_get_parent(child, &parent) >= 0; child = parent) { + (void) sd_device_get_devpath(parent, &str); + printf(" looking at parent device '%s':\n", str); + (void) sd_device_get_sysname(parent, &str); + printf(" KERNELS==\"%s\"\n", str); + if (sd_device_get_subsystem(parent, &str) < 0) + str = ""; + printf(" SUBSYSTEMS==\"%s\"\n", str); + if (sd_device_get_driver(parent, &str) < 0) + str = ""; + printf(" DRIVERS==\"%s\"\n", str); + print_all_attributes(parent, "ATTRS"); + } + + 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 r; + + r = sd_device_enumerator_allow_uninitialized(e); + if (r < 0) + return r; + + r = device_enumerator_scan_devices(e); + if (r < 0) + return r; + + FOREACH_DEVICE_AND_SUBSYSTEM(e, d) + 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" + , 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' }, + { "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:RVh", 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 '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 (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..3dde3f3 --- /dev/null +++ b/src/udev/udevadm-monitor.c @@ -0,0 +1,263 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <errno.h> +#include <getopt.h> +#include <signal.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "device-monitor-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" + +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) { + const char *action = NULL, *devpath = NULL, *subsystem = NULL; + MonitorNetlinkGroup group = PTR_TO_INT(userdata); + struct timespec ts; + + assert(device); + assert(IN_SET(group, MONITOR_GROUP_UDEV, MONITOR_GROUP_KERNEL)); + + (void) sd_device_get_property_value(device, "ACTION", &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, + 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; + Iterator i; + 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, i) { + 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, i) { + 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': { + _cleanup_free_ char *tag = NULL; + + r = set_ensure_allocated(&arg_tag_filter, &string_hash_ops); + if (r < 0) + return r; + + tag = strdup(optarg); + if (!tag) + return -ENOMEM; + + r = set_put(arg_tag_filter, tag); + if (r < 0) + return r; + + tag = NULL; + 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_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..8d9c450 --- /dev/null +++ b/src/udev/udevadm-settle.c @@ -0,0 +1,146 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 <string.h> +#include <unistd.h> + +#include "libudev-util.h" +#include "time-util.h" +#include "udevadm.h" +#include "udev-ctrl.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; +} + +int settle_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_queue_unrefp) struct udev_queue *queue = NULL; + struct pollfd pfd; + usec_t deadline; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (running_in_chroot() > 0) { + log_info("Running in chroot, ignoring request."); + return 0; + } + + 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; + + uctrl = udev_ctrl_new(); + if (uctrl) { + r = udev_ctrl_send_ping(uctrl, MAX(5 * USEC_PER_SEC, arg_timeout)); + if (r < 0) { + log_debug_errno(r, "Failed to connect to udev daemon: %m"); + return 0; + } + } + } + + queue = udev_queue_new(NULL); + if (!queue) + return log_error_errno(errno, "Failed to get udev queue: %m"); + + r = udev_queue_get_fd(queue); + if (r < 0) { + log_debug_errno(r, "Queue is empty, nothing to watch."); + return 0; + } + + pfd = (struct pollfd) { + .events = POLLIN, + .fd = r, + }; + + 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 */ + if (poll(&pfd, 1, MSEC_PER_SEC) > 0 && pfd.revents & 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..22c3184 --- /dev/null +++ b/src/udev/udevadm-test-builtin.c @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#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; + enum udev_builtin_cmd 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..54c525e --- /dev/null +++ b/src/udev/udevadm-test.c @@ -0,0 +1,149 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 "string-util.h" +#include "strxcpyx.h" +#include "udev-builtin.h" +#include "udev.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 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': + 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 (!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; + Iterator i; + 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_new(&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, NULL, rules); + + FOREACH_DEVICE_PROPERTY(dev, key, value) + printf("%s=%s\n", key, value); + + HASHMAP_FOREACH_KEY(val, cmd, event->run_list, i) { + char program[UTIL_PATH_SIZE]; + + 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..9532946 --- /dev/null +++ b/src/udev/udevadm-trigger.c @@ -0,0 +1,379 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <errno.h> +#include <getopt.h> + +#include "sd-device.h" +#include "sd-event.h" + +#include "device-enumerator-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; + + 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) { + log_debug_errno(r, "Failed to write '%s' to '%s', ignoring: %m", action, filename); + continue; + } + + if (settle_set) { + r = set_put_strdup(settle_set, syspath); + if (r < 0) + return log_oom(); + } + } + + return 0; +} + +static int device_monitor_handler(sd_device_monitor *m, sd_device *dev, void *userdata) { + 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); + + if (!set_remove(settle_set, syspath)) + 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 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_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 (STR_IN_SET(optarg, "add", "remove", "change")) + action = optarg; + else + log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown action '%s'", 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 = sd_device_enumerator_add_match_parent(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 = sd_device_enumerator_add_match_parent(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 (!arg_dry_run || ping) { + r = must_be_root(); + if (r < 0) + return r; + } + + if (ping) { + _cleanup_(udev_ctrl_unrefp) struct udev_ctrl *uctrl = NULL; + + uctrl = udev_ctrl_new(); + if (!uctrl) + return log_oom(); + + r = udev_ctrl_send_ping(uctrl, ping_timeout_usec); + if (r < 0) + return log_error_errno(r, "Failed to connect to udev daemon: %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 = sd_device_enumerator_add_match_parent(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); + 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_set); + 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..557982b --- /dev/null +++ b/src/udev/udevadm-util.c @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#include <errno.h> + +#include "alloc-util.h" +#include "device-private.h" +#include "path-util.h" +#include "udevadm-util.h" +#include "unit-name.h" + +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. */ + if (unit_name_is_valid(id, UNIT_NAME_PLAIN) && + unit_name_to_type(id) == UNIT_DEVICE) { + r = unit_name_to_path(id, &path); + if (r < 0) + return log_debug_errno(r, "Failed to convert \"%s\" to a device path: %m", id); + id = path; + } + } + + if (path_startswith(id, "/sys/")) + return sd_device_new_from_syspath(ret, id); + + if (path_startswith(id, "/dev/")) { + struct stat st; + + if (stat(id, &st) < 0) + return -errno; + + return device_new_from_stat_rdev(ret, &st); + } + + return -EINVAL; +} diff --git a/src/udev/udevadm-util.h b/src/udev/udevadm-util.h new file mode 100644 index 0000000..59e8902 --- /dev/null +++ b/src/udev/udevadm-util.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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..2819431 --- /dev/null +++ b/src/udev/udevadm.c @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ + +#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 "udev-util.h" +#include "verbs.h" +#include "util.h" + +static int help(void) { + static const char * 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; + + 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()); + + mac_selinux_init(); + 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..86b24af --- /dev/null +++ b/src/udev/udevadm.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +#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..0303f36 --- /dev/null +++ b/src/udev/udevd.c @@ -0,0 +1,1855 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 <signal.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.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/socket.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 "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 "udev-builtin.h" +#include "udev-ctrl.h" +#include "udev-util.h" +#include "udev-watch.h" +#include "udev.h" +#include "user-util.h" + +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; + +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; + struct udev_ctrl_connection *ctrl_conn_blocking; + int fd_inotify; + int worker_watch[2]; + + sd_event_source *ctrl_event; + sd_event_source *uevent_event; + 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, SIGKILL); + 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; + uint64_t usec; + + 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; + + assert_se(sd_event_now(e, CLOCK_MONOTONIC, &usec) >= 0); + + (void) sd_event_add_time(e, &event->timeout_warning_event, CLOCK_MONOTONIC, + usec + udev_warn_timeout(arg_event_timeout_usec), USEC_PER_SEC, on_event_timeout_warning, event); + + (void) sd_event_add_time(e, &event->timeout_event, CLOCK_MONOTONIC, + usec + arg_event_timeout_usec, USEC_PER_SEC, on_event_timeout, event); +} + +static void manager_clear_for_worker(Manager *manager) { + assert(manager); + + manager->ctrl_event = sd_event_source_unref(manager->ctrl_event); + manager->uevent_event = sd_event_source_unref(manager->uevent_event); + 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_conn_blocking = udev_ctrl_connection_unref(manager->ctrl_conn_blocking); + 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(); + + 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. + */ + + r = sd_device_get_property_value(dev, "ACTION", &val); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get the value of property 'ACTION': %m"); + + if (streq(val, "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_process_device(Manager *manager, sd_device *dev) { + _cleanup_(udev_event_freep) UdevEvent *udev_event = NULL; + _cleanup_close_ int fd_lock = -1; + const char *seqnum, *action; + int r; + + assert(manager); + assert(dev); + + r = sd_device_get_property_value(dev, "SEQNUM", &seqnum); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get SEQNUM: %m"); + + r = sd_device_get_property_value(dev, "ACTION", &action); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get ACTION: %m"); + + log_device_debug(dev, "Processing device (SEQNUM=%s, ACTION=%s)", seqnum, 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 < 0) + return r; + + /* apply rules, create node, symlinks */ + udev_event_execute_rules(udev_event, arg_event_timeout_usec, manager->properties, manager->rules); + udev_event_execute_run(udev_event, arg_event_timeout_usec); + + if (!manager->rtnl) + /* in case rtnl was initialized */ + manager->rtnl = sd_netlink_ref(udev_event->rtnl); + + /* apply/restore 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"); + } + + log_device_debug(dev, "Device (SEQNUM=%s, ACTION=%s) processed", seqnum, 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 < 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); + + unsetenv("NOTIFY_SOCKET"); + + 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) { + struct worker *worker; + Iterator i; + int r; + + assert(manager); + assert(event); + + HASHMAP_FOREACH(worker, manager->workers, i) { + 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) { + if (arg_children_max > 1) + log_debug("Maximum number (%u) of children reached.", hashmap_size(manager->workers)); + return; + } + + /* 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; + const char *val, *action; + struct event *event; + uint64_t seqnum; + int r; + + assert(manager); + assert(dev); + + /* only one process can add events to the queue */ + if (manager->pid == 0) + manager->pid = getpid_cached(); + + assert(manager->pid == getpid_cached()); + + /* We only accepts devices received by device monitor. */ + r = sd_device_get_property_value(dev, "SEQNUM", &val); + if (r < 0) + return r; + + r = safe_atou64(val, &seqnum); + if (r < 0) + return r; + + if (seqnum == 0) + return -EINVAL; + + /* Refuse devices do not have ACTION property. */ + r = sd_device_get_property_value(dev, "ACTION", &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, action); + + return 0; +} + +static void manager_kill_workers(Manager *manager) { + struct worker *worker; + Iterator i; + + assert(manager); + + HASHMAP_FOREACH(worker, manager->workers, i) { + 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")) + return true; + } + + /* check network device ifindex */ + if (ifindex > 0) { + int i; + + if (sd_device_get_ifindex(loop_event->dev, &i) >= 0 && + ifindex == i) + return true; + } + + if (sd_device_get_devpath(loop_event->dev, &loop_devpath) < 0) + continue; + + /* check our old name */ + if (devpath_old && streq(devpath_old, loop_devpath)) { + event->delaying_seqnum = loop_event->seqnum; + return true; + } + + 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) { + /* devices names might have changed/swapped in the meantime */ + if (major(devnum) != 0 || ifindex > 0) + continue; + event->delaying_seqnum = loop_event->seqnum; + return true; + } + + /* parent device event found */ + if (devpath[common] == '/') { + event->delaying_seqnum = loop_event->seqnum; + return true; + } + + /* child device event found */ + if (loop_devpath[common] == '/') { + event->delaying_seqnum = loop_event->seqnum; + return true; + } + } + + return false; +} + +static int on_exit_timeout(sd_event_source *s, uint64_t usec, void *userdata) { + Manager *manager = userdata; + + assert(manager); + + log_error("Giving up waiting for workers to finish."); + sd_event_exit(manager->event, -ETIMEDOUT); + + return 1; +} + +static void manager_exit(Manager *manager) { + uint64_t usec; + int r; + + 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_event = sd_event_source_unref(manager->ctrl_event); + 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->uevent_event = sd_event_source_unref(manager->uevent_event); + manager->monitor = sd_device_monitor_unref(manager->monitor); + + /* discard queued events and kill workers */ + event_queue_cleanup(manager, EVENT_QUEUED); + manager_kill_workers(manager); + + assert_se(sd_event_now(manager->event, CLOCK_MONOTONIC, &usec) >= 0); + + r = sd_event_add_time(manager->event, NULL, CLOCK_MONOTONIC, + usec + 30 * USEC_PER_SEC, USEC_PER_SEC, on_exit_timeout, manager); + if (r < 0) + return; +} + +/* 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_new(&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), + }; + union { + struct cmsghdr cmsghdr; + uint8_t buf[CMSG_SPACE(sizeof(struct ucred))]; + } control = {}; + struct msghdr msghdr = { + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct cmsghdr *cmsg; + ssize_t size; + struct ucred *ucred = NULL; + struct worker *worker; + + size = recvmsg(fd, &msghdr, MSG_DONTWAIT); + if (size < 0) { + if (errno == EINTR) + continue; + else if (errno == EAGAIN) + /* nothing more to read */ + break; + + return log_error_errno(errno, "Failed to receive message: %m"); + } else if (size != sizeof(struct worker_message)) { + log_warning("Ignoring worker message with invalid size %zi bytes", size); + continue; + } + + CMSG_FOREACH(cmsg, &msghdr) + if (cmsg->cmsg_level == SOL_SOCKET && + cmsg->cmsg_type == SCM_CREDENTIALS && + cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) + ucred = (struct ucred*) CMSG_DATA(cmsg); + + 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(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *manager = userdata; + _cleanup_(udev_ctrl_connection_unrefp) struct udev_ctrl_connection *ctrl_conn = NULL; + _cleanup_(udev_ctrl_msg_unrefp) struct udev_ctrl_msg *ctrl_msg = NULL; + const char *str; + int i, r; + + assert(manager); + + ctrl_conn = udev_ctrl_get_connection(manager->ctrl); + if (!ctrl_conn) + return 1; + + ctrl_msg = udev_ctrl_receive_msg(ctrl_conn); + if (!ctrl_msg) + return 1; + + i = udev_ctrl_get_set_log_level(ctrl_msg); + if (i >= 0) { + log_debug("Received udev control message (SET_LOG_LEVEL), setting log_priority=%i", i); + log_set_max_level_realm(LOG_REALM_UDEV, i); + log_set_max_level_realm(LOG_REALM_SYSTEMD, i); + manager_kill_workers(manager); + } + + if (udev_ctrl_get_stop_exec_queue(ctrl_msg) > 0) { + log_debug("Received udev control message (STOP_EXEC_QUEUE)"); + manager->stop_exec_queue = true; + } + + if (udev_ctrl_get_start_exec_queue(ctrl_msg) > 0) { + log_debug("Received udev control message (START_EXEC_QUEUE)"); + manager->stop_exec_queue = false; + event_queue_start(manager); + } + + if (udev_ctrl_get_reload(ctrl_msg) > 0) { + log_debug("Received udev control message (RELOAD)"); + manager_reload(manager); + } + + str = udev_ctrl_get_set_env(ctrl_msg); + if (str) { + _cleanup_free_ char *key = NULL, *val = NULL, *old_key = NULL, *old_val = NULL; + char *eq; + + eq = strchr(str, '='); + if (!eq) { + log_error("Invalid key format '%s'", str); + return 1; + } + + key = strndup(str, eq - str); + 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); + } + + i = udev_ctrl_get_set_children_max(ctrl_msg); + if (i >= 0) { + log_debug("Receivd udev control message (SET_MAX_CHILDREN), setting children_max=%i", i); + arg_children_max = i; + + (void) sd_notifyf(false, + "READY=1\n" + "STATUS=Processing with %u children at max", arg_children_max); + } + + if (udev_ctrl_get_ping(ctrl_msg) > 0) + log_debug("Received udev control message (PING)"); + + if (udev_ctrl_get_exit(ctrl_msg) > 0) { + log_debug("Received udev control message (EXIT)"); + manager_exit(manager); + /* keep reference to block the client until we exit + TODO: deal with several blocking exit requests */ + manager->ctrl_conn_blocking = udev_ctrl_connection_ref(ctrl_conn); + } + + return 1; +} + +static int synthesize_change(sd_device *dev) { + const char *subsystem, *sysname, *devname, *syspath, *devtype; + char filename[PATH_MAX]; + 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. + */ + log_debug("Device '%s' is closed, synthesising 'change'", devname); + strscpyl(filename, sizeof(filename), syspath, "/uevent", NULL); + write_string_file(filename, "change", WRITE_STRING_FILE_DISABLE_BUFFER); + + 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; + + log_debug("Device '%s' is closed, synthesising partition '%s' 'change'", devname, n); + strscpyl(filename, sizeof(filename), s, "/uevent", NULL); + write_string_file(filename, "change", WRITE_STRING_FILE_DISABLE_BUFFER); + } + + return 0; + } + + log_debug("Device %s is closed, synthesising 'change'", devname); + strscpyl(filename, sizeof(filename), syspath, "/uevent", NULL); + write_string_file(filename, "change", WRITE_STRING_FILE_DISABLE_BUFFER); + + 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); + + /* 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_priority=<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 + */ +static int parse_proc_cmdline_item(const char *key, const char *value, void *data) { + int r = 0; + + assert(key); + + if (!value) + return 0; + + if (proc_cmdline_key_streq(key, "udev.log_priority")) { + + 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 (startswith(key, "udev.")) + log_warning("Unknown udev kernel command line option \"%s\", ignoring", key); + + 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" + "Manages devices.\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[]) { + 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' }, + {} + }; + + 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 '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, + }; + + manager->ctrl = udev_ctrl_new_from_fd(fd_ctrl); + if (!manager->ctrl) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize udev control socket"); + + r = udev_ctrl_enable_receiving(manager->ctrl); + if (r < 0) + return log_error_errno(r, "Failed to bind udev control socket: %m"); + + r = device_monitor_new_full(&manager->monitor, MONITOR_GROUP_KERNEL, fd_uevent); + if (r < 0) + return log_error_errno(r, "Failed to initialize device monitor: %m"); + + (void) sd_device_monitor_set_receive_buffer_size(manager->monitor, 128 * 1024 * 1024); + + 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, fd_ctrl, r; + + /* 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"); + + fd_ctrl = udev_ctrl_get_fd(manager->ctrl); + if (fd_ctrl < 0) + return log_error_errno(fd_ctrl, "Failed to get udev control socket fd: %m"); + + r = sd_event_add_io(manager->event, &manager->ctrl_event, fd_ctrl, EPOLLIN, on_ctrl_msg, manager); + if (r < 0) + return log_error_errno(r, "Failed to create udev control event source: %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(manager->ctrl_event, 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_new(&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; +} + +static int run(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); + udev_parse_config_full(&arg_children_max, &arg_exec_delay_usec, &arg_event_timeout_usec, &arg_resolve_name_timing); + log_parse_environment(); + log_open(); + + 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) { + cpu_set_t cpu_set; + unsigned long mem_limit; + + arg_children_max = 8; + + if (sched_getaffinity(0, sizeof(cpu_set), &cpu_set) == 0) + arg_children_max += CPU_COUNT(&cpu_set) * 8; + + mem_limit = physical_memory() / (128LU*1024*1024); + arg_children_max = MAX(10U, MIN(arg_children_max, mem_limit)); + + log_debug("Set children_max to %u", arg_children_max); + } + + /* set umask before creating any file/directory */ + r = chdir("/"); + if (r < 0) + return log_error_errno(errno, "Failed to change dir to '/': %m"); + + umask(022); + + r = mac_selinux_init(); + if (r < 0) + return log_error_errno(r, "Could not initialize labelling: %m"); + + r = mkdir_errno_wrapper("/run/udev", 0755); + if (r < 0 && r != -EEXIST) + return log_error_errno(r, "Failed to create /run/udev: %m"); + + dev_setup(NULL, UID_INVALID, GID_INVALID); + + 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(); + + r = set_oom_score_adjust(-1000); + if (r < 0) + log_debug_errno(r, "Failed to adjust OOM score, ignoring: %m"); + } + + r = main_loop(manager); + /* FIXME: move this into manager_free() */ + udev_ctrl_cleanup(manager->ctrl); + return r; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/v4l_id/v4l_id.c b/src/udev/v4l_id/v4l_id.c new file mode 100644 index 0000000..4d48289 --- /dev/null +++ b/src/udev/v4l_id/v4l_id.c @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * 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 == NULL) + return 2; + + fd = open(device, O_RDONLY); + if (fd < 0) + return 3; + + if (ioctl(fd, VIDIOC_QUERYCAP, &v2cap) == 0) { + printf("ID_V4L_VERSION=2\n"); + printf("ID_V4L_PRODUCT=%s\n", v2cap.card); + printf("ID_V4L_CAPABILITIES=:"); + if ((v2cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) > 0 || + (v2cap.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) > 0) + printf("capture:"); + if ((v2cap.capabilities & V4L2_CAP_VIDEO_OUTPUT) > 0 || + (v2cap.capabilities & V4L2_CAP_VIDEO_OUTPUT_MPLANE) > 0) + printf("video_output:"); + if ((v2cap.capabilities & V4L2_CAP_VIDEO_OVERLAY) > 0) + printf("video_overlay:"); + if ((v2cap.capabilities & V4L2_CAP_AUDIO) > 0) + printf("audio:"); + if ((v2cap.capabilities & V4L2_CAP_TUNER) > 0) + printf("tuner:"); + if ((v2cap.capabilities & V4L2_CAP_RADIO) > 0) + printf("radio:"); + printf("\n"); + } + + return 0; +} |