/* * Copyright (C) 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include "fu-ata-device.h" #define FU_ATA_IDENTIFY_SIZE 512 /* bytes */ #define FU_ATA_BLOCK_SIZE 512 /* bytes */ struct ata_tf { guint8 dev; guint8 command; guint8 error; guint8 status; guint8 feat; guint8 nsect; guint8 lbal; guint8 lbam; guint8 lbah; }; #define ATA_USING_LBA (1 << 6) #define ATA_STAT_DRQ (1 << 3) #define ATA_STAT_ERR (1 << 0) #define ATA_OP_IDENTIFY 0xec #define ATA_OP_FLUSH_CACHE 0xe7 #define ATA_OP_DOWNLOAD_MICROCODE 0x92 #define ATA_OP_STANDBY_IMMEDIATE 0xe0 #define ATA_SUBCMD_MICROCODE_OBSOLETE 0x01 #define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE 0x03 #define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK 0x07 #define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS 0x0e #define ATA_SUBCMD_MICROCODE_ACTIVATE 0x0f #define SG_CHECK_CONDITION 0x02 #define SG_DRIVER_SENSE 0x08 #define SG_ATA_12 0xa1 #define SG_ATA_12_LEN 12 #define SG_ATA_PROTO_NON_DATA (3 << 1) #define SG_ATA_PROTO_PIO_IN (4 << 1) #define SG_ATA_PROTO_PIO_OUT (5 << 1) enum { SG_CDB2_TLEN_NODATA = 0 << 0, SG_CDB2_TLEN_FEAT = 1 << 0, SG_CDB2_TLEN_NSECT = 2 << 0, SG_CDB2_TLEN_BYTES = 0 << 2, SG_CDB2_TLEN_SECTORS = 1 << 2, SG_CDB2_TDIR_TO_DEV = 0 << 3, SG_CDB2_TDIR_FROM_DEV = 1 << 3, SG_CDB2_CHECK_COND = 1 << 5, }; struct _FuAtaDevice { FuUdevDevice parent_instance; guint pci_depth; guint usb_depth; guint16 transfer_blocks; guint8 transfer_mode; guint32 oui; }; G_DEFINE_TYPE(FuAtaDevice, fu_ata_device, FU_TYPE_UDEV_DEVICE) guint8 fu_ata_device_get_transfer_mode(FuAtaDevice *self) { return self->transfer_mode; } guint16 fu_ata_device_get_transfer_blocks(FuAtaDevice *self) { return self->transfer_blocks; } static gchar * fu_ata_device_get_string(const guint16 *buf, guint start, guint end) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = start; i <= end; i++) { g_string_append_c(str, (gchar)(buf[i] >> 8)); g_string_append_c(str, (gchar)(buf[i] & 0xff)); } /* remove whitespace before returning */ if (str->len > 0) { g_strstrip(str->str); if (str->str[0] == '\0') return NULL; } return g_string_free(g_steal_pointer(&str), FALSE); } static void fu_ata_device_to_string(FuDevice *device, guint idt, GString *str) { FuAtaDevice *self = FU_ATA_DEVICE(device); fu_common_string_append_kx(str, idt, "TransferMode", self->transfer_mode); fu_common_string_append_kx(str, idt, "TransferBlocks", self->transfer_blocks); if (self->oui != 0x0) fu_common_string_append_kx(str, idt, "OUI", self->oui); fu_common_string_append_ku(str, idt, "PciDepth", self->pci_depth); fu_common_string_append_ku(str, idt, "UsbDepth", self->usb_depth); } /* https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifiers-for-ide-devices */ static gchar * fu_ata_device_pad_string_for_id(const gchar *name) { GString *str = g_string_new(name); fu_common_string_replace(str, " ", "_"); for (guint i = str->len; i < 40; i++) g_string_append_c(str, '_'); return g_string_free(str, FALSE); } static gchar * fu_ata_device_get_guid_safe(const guint16 *buf, guint16 addr_start) { if (!fu_common_guid_is_plausible((guint8 *)(buf + addr_start))) return NULL; return fwupd_guid_to_string((const fwupd_guid_t *)(buf + addr_start), FWUPD_GUID_FLAG_MIXED_ENDIAN); } static void fu_ata_device_parse_id_maybe_dell(FuAtaDevice *self, const guint16 *buf) { g_autofree gchar *component_id = NULL; g_autofree gchar *guid_efi = NULL; g_autofree gchar *guid_id = NULL; g_autofree gchar *guid = NULL; /* add extra component ID if set */ component_id = fu_ata_device_get_string(buf, 137, 140); if (component_id == NULL || !g_str_is_ascii(component_id) || strlen(component_id) < 6) { g_debug("invalid component ID, skipping"); return; } /* do not add the FuUdevDevice instance IDs as generic firmware * should not be used on these OEM-specific devices */ fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_NO_AUTO_INSTANCE_IDS); /* add instance ID *and* GUID as using no-auto-instance-ids */ guid_id = g_strdup_printf("STORAGE-DELL-%s", component_id); fu_device_add_instance_id(FU_DEVICE(self), guid_id); guid = fwupd_guid_hash_string(guid_id); fu_device_add_guid(FU_DEVICE(self), guid); /* also add the EFI GUID */ guid_efi = fu_ata_device_get_guid_safe(buf, 129); if (guid_efi != NULL) fu_device_add_guid(FU_DEVICE(self), guid_efi); /* owned by Dell */ fu_device_set_vendor(FU_DEVICE(self), "Dell"); fu_device_add_vendor_id(FU_DEVICE(self), "ATA:0x1028"); } static void fu_ata_device_parse_vendor_name(FuAtaDevice *self, const gchar *name) { struct { const gchar *prefix; /* in CAPS */ guint16 vid; const gchar *name; } map_name[] = {/* vendor matches */ {"ADATA*", 0x1cc1, "ADATA"}, {"APACER*", 0x0000, "Apacer"}, /* not in pci.ids */ {"APPLE*", 0x106b, "Apple"}, {"CORSAIR*", 0x1987, "Corsair"}, /* identifies as Phison */ {"CRUCIAL*", 0xc0a9, "Crucial"}, {"FUJITSU*", 0x10cf, "Fujitsu"}, {"GIGABYTE*", 0x1458, "Gigabyte"}, {"HGST*", 0x101c, "Western Digital"}, {"HITACHI*", 0x101c, "Western Digital"}, /* was acquired by WD */ {"HITACHI*", 0x1054, "Hitachi"}, {"HP SSD*", 0x103c, "HP"}, {"INTEL*", 0x8086, "Intel"}, {"KINGSPEC*", 0x0000, "KingSpec"}, /* not in pci.ids */ {"KINGSTON*", 0x2646, "Kingston"}, {"LITEON*", 0x14a4, "LITE-ON"}, {"MAXTOR*", 0x115f, "Maxtor"}, {"MICRON*", 0x1344, "Micron"}, {"OCZ*", 0x1179, "Toshiba"}, {"PNY*", 0x196e, "PNY"}, {"QEMU*", 0x1b36, "QEMU"}, /* identifies as Red Hat! */ {"SAMSUNG*", 0x144d, "Samsung"}, {"SANDISK*", 0x15b7, "SanDisk"}, {"SEAGATE*", 0x1bb1, "Seagate"}, { "SK HYNIX*", 0x1c5c, "SK hynix", }, {"SUPERMICRO*", 0x15d9, "SuperMicro"}, {"TOSHIBA*", 0x1179, "Toshiba"}, {"WDC*", 0x101c, "Western Digital"}, {NULL, 0x0000, NULL}}; struct { const gchar *prefix; /* in CAPS */ guint16 vid; const gchar *name; } map_fuzzy[] = {/* fuzzy name matches -- also see legacy list at: * https://github.com/linuxhw/hw-probe/blob/master/hw-probe.pl#L647 */ {"001-*", 0x1bb1, "Seagate"}, {"726060*", 0x101c, "Western Digital"}, {"CT*", 0xc0a9, "Crucial"}, {"DT0*", 0x1179, "Toshiba"}, {"EZEX*", 0x101c, "Western Digital"}, {"GB0*", 0x1590, "HPE"}, {"GOODRAM*", 0x1987, "Phison"}, {"H??54*", 0x101c, "Western Digital"}, {"H??72?0*", 0x101c, "Western Digital"}, {"HDWG*", 0x1179, "Toshiba"}, {"M?0??CA*", 0x1179, "Toshiba"}, /* enterprise */ {"M4-CT*", 0xc0a9, "Crucial"}, { "MA*", 0x10cf, "Fujitsu", }, { "MB*", 0x10cf, "Fujitsu", }, {"MK0*", 0x1590, "HPE"}, {"MTFDDAK*", 0x1344, "Micron"}, { "NIM*", 0x0000, "Nimbus", }, /* no PCI ID */ { "SATADOM*", 0x0000, "Innodisk", }, /* no PCI ID */ {"SSD 860*", 0x144d, "Samsung"}, {"SSDPR*", 0x1987, "Phison"}, {"SSDSC?K*", 0x8086, "Intel"}, { "ST*", 0x1bb1, "Seagate", }, {"TEAM*", 0x0000, "Team Group"}, /* not in pci.ids */ {"TS*", 0x8564, "Transcend"}, {"VK0*", 0x1590, "HPE"}, {"WD*", 0x101c, "Western Digital"}, {NULL, 0x0000, NULL}}; struct { const gchar *prefix; /* in CAPS */ guint16 vid; const gchar *name; } map_version[] = {/* fuzzy version matches */ {"CS2111*", 0x196e, "PNY"}, {"S?FM*", 0x1987, "Phison"}, {NULL, 0x0000, NULL}}; g_autofree gchar *name_up = g_ascii_strup(name, -1); g_autofree gchar *vendor_id = NULL; /* find match */ for (guint i = 0; map_name[i].prefix != NULL; i++) { if (fu_common_fnmatch(map_name[i].prefix, name_up)) { name += strlen(map_name[i].prefix) - 1; fu_device_set_vendor(FU_DEVICE(self), map_name[i].name); vendor_id = g_strdup_printf("ATA:0x%X", map_name[i].vid); break; } } /* fall back to fuzzy match */ if (vendor_id == NULL) { for (guint i = 0; map_fuzzy[i].prefix != NULL; i++) { if (fu_common_fnmatch(map_fuzzy[i].prefix, name_up)) { fu_device_set_vendor(FU_DEVICE(self), map_fuzzy[i].name); vendor_id = g_strdup_printf("ATA:0x%X", map_fuzzy[i].vid); break; } } } /* fall back to version */ if (vendor_id == NULL) { g_autofree gchar *version_up = g_ascii_strup(fu_device_get_version(FU_DEVICE(self)), -1); for (guint i = 0; map_version[i].prefix != NULL; i++) { if (fu_common_fnmatch(map_version[i].prefix, version_up)) { fu_device_set_vendor(FU_DEVICE(self), map_version[i].name); vendor_id = g_strdup_printf("ATA:0x%X", map_version[i].vid); break; } } } /* devices without a vendor ID will not be UPGRADABLE */ if (vendor_id != NULL) fu_device_add_vendor_id(FU_DEVICE(self), vendor_id); /* remove leading junk */ while (name[0] == ' ' || name[0] == '_' || name[0] == '-') name += 1; /* if changed */ if (g_strcmp0(fu_device_get_name(FU_DEVICE(self)), name) != 0) fu_device_set_name(FU_DEVICE(self), name); } static gboolean fu_ata_device_parse_id(FuAtaDevice *self, const guint8 *buf, gsize sz, GError **error) { FuDevice *device = FU_DEVICE(self); gboolean has_oui_quirk = FALSE; guint16 xfer_min = 1; guint16 xfer_max = 0xffff; guint16 id[FU_ATA_IDENTIFY_SIZE / 2]; g_autofree gchar *name = NULL; g_autofree gchar *sku = NULL; /* check size */ if (sz != FU_ATA_IDENTIFY_SIZE) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "ID incorrect size, got 0x%02x", (guint)sz); return FALSE; } /* read LE buffer */ for (guint i = 0; i < sz / 2; i++) id[i] = fu_common_read_uint16(buf + (i * 2), G_LITTLE_ENDIAN); /* verify drive correctly supports DOWNLOAD_MICROCODE */ if (!(id[83] & 1 && id[86] & 1)) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "DOWNLOAD_MICROCODE not supported by device"); return FALSE; } fu_ata_device_parse_id_maybe_dell(self, id); /* firmware will be applied when the device restarts */ if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); /* the newer, segmented transfer mode */ if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE || self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS) { xfer_min = id[234]; if (xfer_min == 0x0 || xfer_min == 0xffff) xfer_min = 1; xfer_max = id[235]; if (xfer_max == 0x0 || xfer_max == 0xffff) xfer_max = xfer_min; } /* fall back to a sane block size */ if (self->transfer_blocks == 0x0) self->transfer_blocks = xfer_min; else if (self->transfer_blocks == 0xffff) self->transfer_blocks = xfer_max; /* get values in case the kernel didn't */ if (fu_device_get_serial(device) == NULL) { g_autofree gchar *tmp = NULL; tmp = fu_ata_device_get_string(id, 10, 19); if (tmp != NULL) fu_device_set_serial(device, tmp); } if (fu_device_get_version(device) == NULL) { g_autofree gchar *tmp = NULL; tmp = fu_ata_device_get_string(id, 23, 26); if (tmp != NULL) fu_device_set_version(device, tmp); } /* get OUI if set */ self->oui = ((guint32)(id[108] & 0x0fff)) << 12 | ((guint32)(id[109] & 0xfff0)) >> 4; if (self->oui > 0x0) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("OUI\\%06x", self->oui); fu_device_add_instance_id_full(device, tmp, FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS); has_oui_quirk = fu_device_get_vendor(FU_DEVICE(self)) != NULL; } if (self->oui > 0x0) { g_autofree gchar *vendor_id = NULL; vendor_id = g_strdup_printf("OUI:%06x", self->oui); fu_device_add_vendor_id(device, vendor_id); } /* if not already set using the vendor block or a OUI quirk */ name = fu_ata_device_get_string(id, 27, 46); if (name != NULL) { /* use the name as-is */ if (has_oui_quirk) { fu_device_set_name(FU_DEVICE(self), name); } else { fu_ata_device_parse_vendor_name(self, name); } } /* 8 byte additional product identifier == SKU? */ sku = fu_ata_device_get_string(id, 170, 173); if (sku != NULL) g_debug("SKU=%s", sku); /* add extra GUIDs if none detected from identify block */ if (name != NULL && fu_device_get_guids(device)->len == 0) { g_autofree gchar *name_pad = fu_ata_device_pad_string_for_id(name); if (name_pad != NULL && fu_device_get_version(device) != NULL) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("IDE\\%s%s", name_pad, fu_device_get_version(device)); fu_device_add_instance_id(device, tmp); } if (name_pad != NULL) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("IDE\\0%s", name_pad); fu_device_add_instance_id(device, tmp); } /* add the name fallback */ fu_device_add_instance_id(device, name); } return TRUE; } static gboolean fu_ata_device_probe(FuDevice *device, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(device)); /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_ata_device_parent_class)->probe(device, error)) return FALSE; /* check is valid */ if (g_strcmp0(g_udev_device_get_devtype(udev_device), "disk") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not correct devtype=%s, expected disk", g_udev_device_get_devtype(udev_device)); return FALSE; } if (!g_udev_device_get_property_as_boolean(udev_device, "ID_ATA_SATA") || !g_udev_device_get_property_as_boolean(udev_device, "ID_ATA_DOWNLOAD_MICROCODE")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "has no ID_ATA_DOWNLOAD_MICROCODE"); return FALSE; } /* set the physical ID */ if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "scsi", error)) return FALSE; /* look at the PCI and USB depth to work out if in an external enclosure */ self->pci_depth = fu_udev_device_get_slot_depth(FU_UDEV_DEVICE(device), "pci"); self->usb_depth = fu_udev_device_get_slot_depth(FU_UDEV_DEVICE(device), "usb"); if (self->pci_depth <= 2 && self->usb_depth <= 2) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); } return TRUE; } static guint64 fu_ata_device_tf_to_pack_id(struct ata_tf *tf) { guint32 lba24 = (tf->lbah << 16) | (tf->lbam << 8) | (tf->lbal); guint32 lbah = tf->dev & 0x0f; return (((guint64)lbah) << 24) | (guint64)lba24; } static gboolean fu_ata_device_command(FuAtaDevice *self, struct ata_tf *tf, gint dxfer_direction, guint timeout_ms, guint8 *dxferp, gsize dxfer_len, GError **error) { guint8 cdb[SG_ATA_12_LEN] = {0x0}; guint8 sb[32] = {0x0}; sg_io_hdr_t io_hdr = {0x0}; /* map _TO_DEV to PIO mode */ if (dxfer_direction == SG_DXFER_TO_DEV) cdb[1] = SG_ATA_PROTO_PIO_OUT; else if (dxfer_direction == SG_DXFER_FROM_DEV) cdb[1] = SG_ATA_PROTO_PIO_IN; else cdb[1] = SG_ATA_PROTO_NON_DATA; /* libata workaround: don't demand sense data for IDENTIFY */ if (dxfer_len > 0) { cdb[2] |= SG_CDB2_TLEN_NSECT | SG_CDB2_TLEN_SECTORS; cdb[2] |= dxfer_direction == SG_DXFER_TO_DEV ? SG_CDB2_TDIR_TO_DEV : SG_CDB2_TDIR_FROM_DEV; } else { cdb[2] = SG_CDB2_CHECK_COND; } /* populate non-LBA48 CDB */ cdb[0] = SG_ATA_12; cdb[3] = tf->feat; cdb[4] = tf->nsect; cdb[5] = tf->lbal; cdb[6] = tf->lbam; cdb[7] = tf->lbah; cdb[8] = tf->dev; cdb[9] = tf->command; if (g_getenv("FWUPD_ATA_VERBOSE") != NULL) { fu_common_dump_raw(G_LOG_DOMAIN, "CBD", cdb, sizeof(cdb)); if (dxfer_direction == SG_DXFER_TO_DEV && dxferp != NULL) { fu_common_dump_raw(G_LOG_DOMAIN, "outgoing_data", dxferp, dxfer_len); } } /* hit hardware */ io_hdr.interface_id = 'S'; io_hdr.mx_sb_len = sizeof(sb); io_hdr.dxfer_direction = dxfer_direction; io_hdr.dxfer_len = dxfer_len; io_hdr.dxferp = dxferp; io_hdr.cmdp = cdb; io_hdr.cmd_len = SG_ATA_12_LEN; io_hdr.sbp = sb; io_hdr.pack_id = fu_ata_device_tf_to_pack_id(tf); io_hdr.timeout = timeout_ms; if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), SG_IO, (guint8 *)&io_hdr, NULL, error)) return FALSE; if (g_getenv("FWUPD_ATA_VERBOSE") != NULL) { g_debug("ATA_%u status=0x%x, host_status=0x%x, driver_status=0x%x", io_hdr.cmd_len, io_hdr.status, io_hdr.host_status, io_hdr.driver_status); fu_common_dump_raw(G_LOG_DOMAIN, "SB", sb, sizeof(sb)); } /* error check */ if (io_hdr.status && io_hdr.status != SG_CHECK_CONDITION) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "bad status: 0x%x", io_hdr.status); return FALSE; } if (io_hdr.host_status) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "bad host status: 0x%x", io_hdr.host_status); return FALSE; } if (io_hdr.driver_status && (io_hdr.driver_status != SG_DRIVER_SENSE)) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "bad driver status: 0x%x", io_hdr.driver_status); return FALSE; } /* repopulate ata_tf */ tf->error = sb[8 + 3]; tf->nsect = sb[8 + 5]; tf->lbal = sb[8 + 7]; tf->lbam = sb[8 + 9]; tf->lbah = sb[8 + 11]; tf->dev = sb[8 + 12]; tf->status = sb[8 + 13]; if (g_getenv("FWUPD_ATA_VERBOSE") != NULL) { g_debug("ATA_%u stat=%02x err=%02x nsect=%02x lbal=%02x " "lbam=%02x lbah=%02x dev=%02x", io_hdr.cmd_len, tf->status, tf->error, tf->nsect, tf->lbal, tf->lbam, tf->lbah, tf->dev); } /* io error */ if (tf->status & (ATA_STAT_ERR | ATA_STAT_DRQ)) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "I/O error, ata_op=0x%02x ata_status=0x%02x ata_error=0x%02x", tf->command, tf->status, tf->error); return FALSE; } /* success */ return TRUE; } static gboolean fu_ata_device_setup(FuDevice *device, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); struct ata_tf tf = {0x0}; guint8 id[FU_ATA_IDENTIFY_SIZE]; /* get ID block */ tf.dev = ATA_USING_LBA; tf.command = ATA_OP_IDENTIFY; tf.nsect = 1; /* 512 bytes */ if (!fu_ata_device_command(self, &tf, SG_DXFER_FROM_DEV, 1000, id, sizeof(id), error)) { g_prefix_error(error, "failed to IDENTIFY: "); return FALSE; } if (g_getenv("FWUPD_ATA_VERBOSE") != NULL) fu_common_dump_raw(G_LOG_DOMAIN, "IDENTIFY", id, sizeof(id)); if (!fu_ata_device_parse_id(self, id, sizeof(id), error)) return FALSE; /* success */ return TRUE; } static gboolean fu_ata_device_activate(FuDevice *device, FuProgress *progress, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); struct ata_tf tf = {0x0}; /* flush cache and put drive in standby to prepare to activate */ tf.dev = ATA_USING_LBA; tf.command = ATA_OP_FLUSH_CACHE; if (!fu_ata_device_command(self, &tf, SG_DXFER_NONE, 120 * 1000, /* a long time! */ NULL, 0, error)) { g_prefix_error(error, "failed to flush cache immediate: "); return FALSE; } tf.command = ATA_OP_STANDBY_IMMEDIATE; if (!fu_ata_device_command(self, &tf, SG_DXFER_NONE, 120 * 1000, /* a long time! */ NULL, 0, error)) { g_prefix_error(error, "failed to standby immediate: "); return FALSE; } /* load the new firmware */ tf.dev = 0xa0 | ATA_USING_LBA; tf.command = ATA_OP_DOWNLOAD_MICROCODE; tf.feat = ATA_SUBCMD_MICROCODE_ACTIVATE; if (!fu_ata_device_command(self, &tf, SG_DXFER_NONE, 120 * 1000, /* a long time! */ NULL, 0, error)) { g_prefix_error(error, "failed to activate firmware: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_ata_device_fw_download(FuAtaDevice *self, guint32 idx, guint32 addr, const guint8 *data, guint32 data_sz, GError **error) { struct ata_tf tf = {0x0}; guint32 block_count = data_sz / FU_ATA_BLOCK_SIZE; guint32 buffer_offset = addr / FU_ATA_BLOCK_SIZE; /* write block */ tf.dev = 0xa0 | ATA_USING_LBA; tf.command = ATA_OP_DOWNLOAD_MICROCODE; tf.feat = self->transfer_mode; tf.nsect = block_count & 0xff; tf.lbal = block_count >> 8; tf.lbam = buffer_offset & 0xff; tf.lbah = buffer_offset >> 8; if (!fu_ata_device_command(self, &tf, SG_DXFER_TO_DEV, 120 * 1000, /* a long time! */ (guint8 *)data, data_sz, error)) { g_prefix_error(error, "failed to write firmware @0x%0x: ", (guint)addr); return FALSE; } /* check drive status */ if (tf.nsect == 0x0) return TRUE; /* drive wants more data, or thinks it is all done */ if (tf.nsect == 0x1 || tf.nsect == 0x2) return TRUE; /* the offset was set up incorrectly */ if (tf.nsect == 0x4) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "alignment error"); return FALSE; } /* other error */ g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "unknown return code 0x%02x", tf.nsect); return FALSE; } static gboolean fu_ata_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); guint32 chunksz = (guint32)self->transfer_blocks * FU_ATA_BLOCK_SIZE; guint max_size = 0xffff * FU_ATA_BLOCK_SIZE; g_autoptr(GBytes) fw = NULL; g_autoptr(GPtrArray) chunks = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* only one block allowed */ if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK) max_size = 0xffff; /* check is valid */ if (g_bytes_get_size(fw) > max_size) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware is too large, maximum size is %u", max_size); return FALSE; } if (g_bytes_get_size(fw) % FU_ATA_BLOCK_SIZE != 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware is not multiple of block size %i", FU_ATA_BLOCK_SIZE); return FALSE; } /* write each block */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); chunks = fu_chunk_array_new_from_bytes(fw, 0x00, 0x00, chunksz); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_ata_device_fw_download(self, fu_chunk_get_idx(chk), fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } fu_progress_set_percentage_full(progress, (gsize)i + 1, (gsize)chunks->len); } /* success! */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); return TRUE; } static gboolean fu_ata_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); if (g_strcmp0(key, "AtaTransferMode") == 0) { guint64 tmp = fu_common_strtoull(value); if (tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE && tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS && tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "AtaTransferMode only supports " "values 0x3, 0x7 or 0xe"); return FALSE; } self->transfer_mode = (guint8)tmp; return TRUE; } if (g_strcmp0(key, "AtaTransferBlocks") == 0) { guint64 tmp = fu_common_strtoull(value); if (tmp > 0xffff) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "AtaTransferBlocks only supports " "values <= 0xffff"); return FALSE; } self->transfer_blocks = (guint16)tmp; return TRUE; } g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_ata_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98); /* write */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2); /* reload */ } static void fu_ata_device_init(FuAtaDevice *self) { /* we chose this default as _DOWNLOAD_CHUNKS_ACTIVATE applies the * firmware straight away and the kernel might not like the unexpected * ATA restart and panic */ self->transfer_mode = ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_INHERIT_ACTIVATION); fu_device_set_summary(FU_DEVICE(self), "ATA drive"); fu_device_add_icon(FU_DEVICE(self), "drive-harddisk"); fu_device_add_protocol(FU_DEVICE(self), "org.t13.ata"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_udev_device_set_flags(FU_UDEV_DEVICE(self), FU_UDEV_DEVICE_FLAG_OPEN_READ); } static void fu_ata_device_finalize(GObject *object) { G_OBJECT_CLASS(fu_ata_device_parent_class)->finalize(object); } static void fu_ata_device_class_init(FuAtaDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); object_class->finalize = fu_ata_device_finalize; klass_device->to_string = fu_ata_device_to_string; klass_device->set_quirk_kv = fu_ata_device_set_quirk_kv; klass_device->setup = fu_ata_device_setup; klass_device->activate = fu_ata_device_activate; klass_device->write_firmware = fu_ata_device_write_firmware; klass_device->probe = fu_ata_device_probe; klass_device->set_progress = fu_ata_device_set_progress; } FuAtaDevice * fu_ata_device_new_from_blob(FuContext *ctx, const guint8 *buf, gsize sz, GError **error) { g_autoptr(FuAtaDevice) self = NULL; self = g_object_new(FU_TYPE_ATA_DEVICE, "context", ctx, NULL); if (!fu_ata_device_parse_id(self, buf, sz, error)) return NULL; return g_steal_pointer(&self); }