fwupd/plugins/ata/fu-ata-device.c
Richard Hughes 40cd18fa97 Allow using a per-device global percentage completion
It's actually quite hard to build a front-end for fwupd at the moment
as you're never sure when the progress bar is going to zip back to 0%
and start all over again. Some plugins go 0..100% for write, others
go 0..100% for erase, then again for write, then *again* for verify.

By creating a helper object we can easily split up the progress of the
specific task, e.g. write_firmware().

We can encode at the plugin level "the erase takes 50% of the time, the
write takes 40% and the read takes 10%". This means we can have a
progressbar which goes up just once at a consistent speed.
2021-09-13 14:28:15 +01:00

917 lines
26 KiB
C

/*
* Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <scsi/sg.h>
#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);
}