/* * zdev - Modify and display the persistent configuration of devices * * Copyright IBM Corp. 2017 * * s390-tools is free software; you can redistribute it and/or modify * it under the terms of the MIT license. See LICENSE for details. */ #include #include #include #include #include #include #include #include #include #include #include #include "attrib.h" #include "ccw.h" #include "ccwgroup.h" #include "dasd.h" #include "device.h" #include "export.h" #include "firmware.h" #include "misc.h" #include "qeth.h" #include "subtype.h" #include "zfcp_host.h" #include "zfcp_lun.h" /* In-memory firmware file representation. */ struct fw_file { const char *name; char *buffer; size_t size; char *last_access; size_t last_size; }; /* Record access to fields of the buffered file for use in warning messages. */ #define fwacc(f, x) ((f)->last_access = (char *) &(x), \ (f)->last_size = sizeof(x), x) /* * Firmware file format definitions. */ /* Firmware file header. */ struct fw_filehdr { uint32_t magic; uint16_t ver; uint16_t hdr_len; uint32_t file_len; uint32_t seq; uint32_t zeroes; uint16_t de_count; char unused[10]; } __packed; #define FW_HDR_MAGIC 0x7a646576 /* ASCII "zdev" */ #define FW_HDR_VER_Z14 0x0000 /* I/O device ID. */ struct fw_iodevid { uint8_t cssid; uint8_t ssid; uint16_t devno; } __packed; #define FW_IODEVID_FLAG_MCSS 0x01 /* Device setting. */ struct fw_setting { uint16_t len; uint8_t key_type; uint8_t key_len; uint8_t val_type; uint8_t val_len; char data[]; } __packed; #define FW_SETTING_KEYTYPE_ASCII 0x00 #define FW_SETTING_VALTYPE_ASCII 0x00 #define FW_SETTING_VALTYPE_UINT 0x01 /* Device settings list. */ struct fw_setlist { uint16_t len; char data[]; } __packed; /* Device entry header. */ struct fw_dehdr { uint16_t type; uint16_t len; uint32_t seq; } __packed; #define FW_DE_HDR_TYPE_DASD 0x0001 #define FW_DE_HDR_TYPE_ZFCP_HOST 0x0002 #define FW_DE_HDR_TYPE_ZFCP_LUN 0x0003 #define FW_DE_HDR_TYPE_QETH 0x0004 /* DASD device entry. */ struct fw_dasd { struct fw_dehdr hdr; uint8_t id_flags; struct fw_iodevid id; char settings[]; } __packed; /* zFCP host device entry. */ struct fw_zfcp_host { struct fw_dehdr hdr; uint8_t id_flags; struct fw_iodevid id; char settings[]; } __packed; /* zFCP LUN device entry. */ struct fw_zfcp_lun { struct fw_dehdr hdr; uint8_t id_flags; struct fw_iodevid id; uint64_t wwpn; uint64_t fcp_lun; char settings[]; } __packed; /* QETH device entry. */ struct fw_qeth { struct fw_dehdr hdr; uint8_t id_flags; struct fw_iodevid read_id; struct fw_iodevid write_id; struct fw_iodevid data_id; char settings[]; } __packed; /* Emit a warning that refers to a position in a firmware file. */ static void fwwarn(struct fw_file *f, const char *fmt, ...) { va_list args; off_t start = (off_t) (f->last_access - f->buffer), end = start + f->last_size - 1; fprintf(stderr, "%s: ", f->name); if (start == end) fprintf(stderr, "Byte 0x%zx: ", start); else fprintf(stderr, "Bytes 0x%zx-0x%zx: ", start, end); va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); fprintf(stderr, "\n"); } /* Basic file format header sanity check. */ static bool check_header(struct fw_file *f, struct fw_filehdr *hdr) { if (fwacc(f, hdr->magic) != FW_HDR_MAGIC) fwwarn(f, "Invalid file magic (0x%08x)", hdr->magic); else if (fwacc(f, hdr->ver) != FW_HDR_VER_Z14) fwwarn(f, "Unsupported file version (0x%04x)", hdr->ver); else return true; return false; } #define READ_RETRY 3 /* Read a firmware configuration file. */ static exit_code_t read_fw(struct fw_file *file, FILE *fd, const char *filename) { struct fw_file f = { NULL }; struct fw_filehdr *hdr; char *buffer, *buffer2; size_t size, size2; int retry; exit_code_t rc = EXIT_OK; for (retry = 0; retry < READ_RETRY; retry++) { /* Read complete file once */ rc = misc_read_fd(fd, (void **) &buffer, &size); if (rc) { warn("%s: Could not read file", filename); return rc; } if (!buffer) { /* Empty file - skip silently as this is the default * on machines without firmware support. */ return rc; } /* Re-read complete file to detect in-flight modifications. */ if (fseek(fd, 0, SEEK_SET) == -1) { /* Could be a pipe, socket, or FIFO - accept v1. */ break; } rc = misc_read_fd(fd, (void **) &buffer2, &size2); if (rc || !buffer2) { /* Could not get second version - accept v1. */ break; } if (size == size2 && memcmp(buffer, buffer2, size) == 0) { /* No change */ free(buffer2); break; } free(buffer); free(buffer2); } if (retry >= READ_RETRY) { warnx("%s: File changed %d times while reading - aborting", filename, retry); return EXIT_RUNTIME_ERROR; } /* Perform basic checks */ f.name = filename; f.buffer = buffer; f.size = size; hdr = (void *) buffer; if (!check_header(&f, hdr)) { free(buffer); return EXIT_FORMAT_ERROR; } if (fwacc(&f, hdr->file_len) > size) { fwwarn(&f, "File length mismatch (expect %zu) - adjusting", size); hdr->file_len = size; } *file = f; return EXIT_OK; } /* Return textual representation of a device entry type. */ static const char *type_to_str(uint16_t type) { switch (type) { case FW_DE_HDR_TYPE_DASD: return "dasd"; case FW_DE_HDR_TYPE_ZFCP_HOST: return "zfcp-host"; case FW_DE_HDR_TYPE_ZFCP_LUN: return "zfcp-lun"; case FW_DE_HDR_TYPE_QETH: return "qeth"; default: return ""; } } /* Convert a binary format device setting of the specified length to integer. */ static unsigned long parse_value(char *data, uint8_t len) { switch (len) { case 1: return (unsigned long) *((uint8_t *) data); case 2: return (unsigned long) *((uint16_t *) data); case 4: return (unsigned long) *((uint32_t *) data); case 8: return (unsigned long) *((uint64_t *) data); default: return 0; } } /* Perform sanity checks on device setting. */ static bool check_setting(struct fw_file *f, struct fw_setting *set) { /* Key sanity checks */ if (fwacc(f, set->key_type) != FW_SETTING_KEYTYPE_ASCII) { fwwarn(f, "Unsupported key type: %d", set->key_type); return false; } if (fwacc(f, set->key_len) < 1) { fwwarn(f, "Unsupported key length: %d", set->key_len); return false; } if (sizeof(struct fw_setting) + fwacc(f, set->key_len) > set->len) { fwwarn(f, "Key length exceeds setting"); return false; } if (fwacc(f, set->data[set->key_len - 1])) { fwwarn(f, "Key not null-terminated"); return false; } /* Value sanity checks */ if (fwacc(f, set->val_type) != FW_SETTING_VALTYPE_UINT && fwacc(f, set->val_type) != FW_SETTING_VALTYPE_ASCII) { fwwarn(f, "Unsupported value type: %d", set->val_type); return false; } if (fwacc(f, set->val_len) < 1) { fwwarn(f, "Unsupported value length: %d", set->val_len); return false; } if (sizeof(struct fw_setting) + set->key_len + fwacc(f, set->val_len) > set->len) { fwwarn(f, "Value length exceeds setting"); return false; } if ((set->val_type == FW_SETTING_VALTYPE_ASCII) && fwacc(f, set->data[set->key_len + set->val_len - 1])) { fwwarn(f, "Value not null-terminated"); return false; } if (set->val_type == FW_SETTING_VALTYPE_UINT) { switch (fwacc(f, set->val_len)) { case 1: case 2: case 4: case 8: break; default: fwwarn(f, "Unsupported integer value length: %d", set->val_len); return false; } } return true; } /* Add a setting to the device. Emit a warning if the setting is not known. */ static void _add_setting(const char *filename, struct device *dev, config_t config, const char *key, const char *value) { struct attrib *a; struct setting_list *list; list = device_get_setting_list(dev, config); a = attrib_find(dev->subtype->dev_attribs, key); if (!a) { warnx("%s: Applying unknown device setting %s=%s", filename, key, value); } setting_list_apply(list, a, key, value); } static void add_setting(const char *filename, struct device *dev, config_t config, const char *key, const char *value) { if (SCOPE_ACTIVE(config)) _add_setting(filename, dev, config_active, key, value); if (SCOPE_PERSISTENT(config)) _add_setting(filename, dev, config_persistent, key, value); if (SCOPE_AUTOCONF(config)) _add_setting(filename, dev, config_autoconf, key, value); } /* Parse a single device setting in firmware format and apply it to the * specified device. */ static void parse_setting(struct fw_file *f, struct fw_setting *set, struct device *dev, config_t config) { char *ascii_key, *ascii_val; unsigned long ulong_val; if (!check_setting(f, set)) return; ascii_key = &set->data[0]; if (set->val_type == FW_SETTING_VALTYPE_UINT) { ulong_val = parse_value(&set->data[set->key_len], set->val_len); ascii_val = misc_asprintf("%lu", ulong_val); add_setting(f->name, dev, config, ascii_key, ascii_val); free(ascii_val); } else { ascii_val = &set->data[set->key_len]; add_setting(f->name, dev, config, ascii_key, ascii_val); } } /* Parse a device settings list in firmware format and apply the resulting * settings to the specified device. */ static void parse_settings(struct fw_file *f, char *data, struct device *dev, config_t config) { struct fw_setlist *list = (struct fw_setlist *) data; struct fw_setting *set; uint16_t off; for (off = sizeof(struct fw_setlist); off < list->len; off += set->len) { set = (struct fw_setting *) &data[off]; if (fwacc(f, set->len) < sizeof(struct fw_setting)) { fwwarn(f, "Setting too short"); break; } if (off + fwacc(f, set->len) > list->len) { fwwarn(f, "Setting too long"); break; } parse_setting(f, set, dev, config); } } /* Perform sanity checks on an I/O device ID. */ static bool check_iodevid(struct fw_file *f, uint8_t *flags, struct fw_iodevid *id) { if (fwacc(f, *flags) & FW_IODEVID_FLAG_MCSS) { fwwarn(f, "Unsupported entry in non-default CSS"); return false; } if (fwacc(f, id->cssid) != 0) { fwwarn(f, "Non-zero CSS-ID"); return false; } return true; } /* Perform sanity checks on a device entry. */ static bool check_de_size(struct fw_file *f, struct fw_dehdr *de, size_t size) { if (fwacc(f, de->len) < size) { fwwarn(f, "Device entry too short (expect %zu)", size); return false; } return true; } /* Convert an I/O device ID to CCW device ID format. */ static void io_to_ccw(struct ccw_devid *c, struct fw_iodevid *i) { c->cssid = i->cssid; c->ssid = i->ssid; c->devno = i->devno; } /* Register a new device configuration. */ static struct device *add_device(struct fw_file *f, struct subtype *st, const char *id, config_t config, struct util_list *objects) { struct device *dev; if (!st->devices) st->devices = device_list_new(st); dev = device_list_find(st->devices, id, NULL); if (!dev) { dev = device_new(st, id); if (!dev) { warnx("%s: Skipping invalid %s device ID %s", f->name, st->name, id); return NULL; } device_list_add(st->devices, dev); } ptrlist_add(objects, object_new(export_device, dev)); /* Prepare device for new settings. */ if (SCOPE_ACTIVE(config)) { setting_list_clear(dev->active.settings); if (dev->subtype->support_definable) dev->active.definable = 1; else dev->active.exists = 1; } if (SCOPE_PERSISTENT(config)) { setting_list_clear(dev->persistent.settings); dev->persistent.exists = 1; } if (SCOPE_AUTOCONF(config)) { setting_list_clear(dev->autoconf.settings); dev->autoconf.exists = 1; } return dev; } /* Parse a DASD device entry. */ static void parse_dasd(struct fw_file *f, struct fw_dehdr *de, config_t config, struct util_list *objects) { struct fw_dasd *dasd = (struct fw_dasd *) de; struct ccw_devid devid; struct device *dev_eckd, *dev_fba; char *id; if (!check_de_size(f, de, sizeof(struct fw_dasd))) return; if (!check_iodevid(f, &dasd->id_flags, &dasd->id)) return; /* Could be either dasd_eckd or dasd_fba - add both entries */ io_to_ccw(&devid, &dasd->id); id = ccw_devid_to_str(&devid); dev_eckd = add_device(f, &dasd_subtype_eckd, id, config, objects); dev_fba = add_device(f, &dasd_subtype_fba, id, config, objects); free(id); if (dasd->hdr.len > sizeof(struct fw_dasd)) { if (dev_eckd) parse_settings(f, dasd->settings, dev_eckd, config); if (dev_fba) parse_settings(f, dasd->settings, dev_fba, config); } } /* Parse a zFCP host device entry. */ static void parse_zfcp_host(struct fw_file *f, struct fw_dehdr *de, config_t config, struct util_list *objects) { struct fw_zfcp_host *zfcp_host = (struct fw_zfcp_host *) de; struct ccw_devid devid; struct device *dev; char *id; if (!check_de_size(f, de, sizeof(struct fw_zfcp_host))) return; if (!check_iodevid(f, &zfcp_host->id_flags, &zfcp_host->id)) return; /* Add zfcp_host entry */ io_to_ccw(&devid, &zfcp_host->id); id = ccw_devid_to_str(&devid); dev = add_device(f, &zfcp_host_subtype, id, config, objects); free(id); if (dev && zfcp_host->hdr.len > sizeof(struct fw_zfcp_host)) parse_settings(f, zfcp_host->settings, dev, config); } /* Parse a zFCP LUN device entry. */ static void parse_zfcp_lun(struct fw_file *f, struct fw_dehdr *de, config_t config, struct util_list *objects) { struct fw_zfcp_lun *zfcp_lun = (struct fw_zfcp_lun *) de; struct zfcp_lun_devid devid; struct device *dev; char *id; if (!check_de_size(f, de, sizeof(struct fw_zfcp_lun))) return; if (!check_iodevid(f, &zfcp_lun->id_flags, &zfcp_lun->id)) return; /* Add zfcp_lun entry */ io_to_ccw(&devid.fcp_dev, &zfcp_lun->id); devid.wwpn = zfcp_lun->wwpn; devid.lun = zfcp_lun->fcp_lun; id = zfcp_lun_devid_to_str(&devid); dev = add_device(f, &zfcp_lun_subtype, id, config, objects); free(id); if (dev && zfcp_lun->hdr.len > sizeof(struct fw_zfcp_lun)) parse_settings(f, zfcp_lun->settings, dev, config); } /* Parse a QETH device entry. */ static void parse_qeth(struct fw_file *f, struct fw_dehdr *de, config_t config, struct util_list *objects) { struct fw_qeth *qeth = (struct fw_qeth *) de; struct ccwgroup_devid devid; struct device *dev; char *id; if (!check_de_size(f, de, sizeof(struct fw_qeth))) return; if (!check_iodevid(f, &qeth->id_flags, &qeth->read_id) || !check_iodevid(f, &qeth->id_flags, &qeth->write_id) || !check_iodevid(f, &qeth->id_flags, &qeth->data_id)) return; /* Add qeth entry */ devid.num = 3; io_to_ccw(&devid.devid[0], &qeth->read_id); io_to_ccw(&devid.devid[1], &qeth->write_id); io_to_ccw(&devid.devid[2], &qeth->data_id); id = ccwgroup_devid_to_str(&devid); dev = add_device(f, &qeth_subtype_qeth, id, config, objects); free(id); if (dev && qeth->hdr.len > sizeof(struct fw_qeth)) parse_settings(f, qeth->settings, dev, config); } /* Parse a firmware file. */ static void parse_fw(struct fw_file *f, long skip, config_t config, struct util_list *objects) { char *data = f->buffer; struct fw_filehdr *hdr = (struct fw_filehdr *) data; struct fw_dehdr *de; uint16_t count = 0; uint32_t off; for (off = hdr->hdr_len; off < hdr->file_len; off += de->len) { count++; de = (struct fw_dehdr *) &data[off]; if (fwacc(f, de->len) == 0) { fwwarn(f, "Empty device entry"); break; } if (off + fwacc(f, de->len) > hdr->file_len) { fwwarn(f, "Device entry too long"); break; } if (skip >= 0 && de->seq <= skip) { debug("Skipping %s entry due to sequence (%08x)\n", type_to_str(de->type), de->seq); continue; } switch (fwacc(f, de->type)) { case FW_DE_HDR_TYPE_DASD: parse_dasd(f, de, config, objects); break; case FW_DE_HDR_TYPE_ZFCP_HOST: parse_zfcp_host(f, de, config, objects); break; case FW_DE_HDR_TYPE_ZFCP_LUN: parse_zfcp_lun(f, de, config, objects); break; case FW_DE_HDR_TYPE_QETH: parse_qeth(f, de, config, objects); break; default: fwwarn(f, "Unknown entry (type=%04x)", de->type); break; } } if (count != fwacc(f, hdr->de_count)) fwwarn(f, "Device entry count mismatch"); } /* Read configuration objects from @fd in firmware file format. Add pointers to * newly allocated struct export_objects to ptrlist @objects. If @skip is a * positive number, skip over entries with a sequence number equal to or * greater than @skip. */ exit_code_t firmware_read(FILE *fd, const char *filename, long skip, config_t config, struct util_list *objects) { struct fw_file file; exit_code_t rc; rc = read_fw(&file, fd, filename); if (rc) return rc; parse_fw(&file, skip, config, objects); free(file.buffer); return rc; } /* Check if @fd refers to a file in binary firmware format. */ bool firmware_detect(FILE *fd) { int c; c = fgetc(fd); ungetc(c, fd); /* Note: A full check would require looking at least at the first 4 * bytes, but fd might be non-seekable (e.g. pipe). Since there is no * way that a textual import file can start with a 'z', looking at * the first char should be enough. */ return c == 'z'; }