fwupd/plugins/scsi/fu-scsi-device.c
2022-06-03 13:47:11 -05:00

315 lines
8.8 KiB
C

/*
* Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <scsi/sg.h>
#include "fu-scsi-device.h"
struct _FuScsiDevice {
FuUdevDevice parent_instance;
guint64 ffu_timeout;
};
G_DEFINE_TYPE(FuScsiDevice, fu_scsi_device, FU_TYPE_UDEV_DEVICE)
#define BUFFER_VENDOR_MODE 0x01
#define BUFFER_DUFS_MODE 0x02
#define BUFFER_FFU_MODE 0x0E
#define BUFFER_EHS_MODE 0x1C
#define SENSE_BUFF_LEN 18
#define WRITE_BUF_CMDLEN 10
#define READ_BUF_CMDLEN 10
#define WRITE_BUFFER_CMD 0x3B
#define READ_BUFFER_CMD 0x3C
#define FU_SCSI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */
static void
fu_scsi_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuScsiDevice *self = FU_SCSI_DEVICE(device);
FU_DEVICE_CLASS(fu_scsi_device_parent_class)->to_string(device, idt, str);
fu_common_string_append_kx(str, idt, "FfuTimeout", self->ffu_timeout);
}
static gboolean
fu_scsi_device_probe(FuDevice *device, GError **error)
{
FuScsiDevice *self = FU_SCSI_DEVICE(device);
GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(device));
guint64 removable = 0;
g_autofree gchar *vendor_id = NULL;
g_autoptr(FuUdevDevice) ufshci_parent = NULL;
/* 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_SCSI")) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"has no ID_SCSI");
return FALSE;
}
/* vendor sanity */
if (g_strcmp0(fu_device_get_vendor(device), "ATA") == 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"no assigned vendor");
return FALSE;
}
vendor_id = g_strdup_printf("SCSI:%s", fu_device_get_vendor(device));
fu_device_add_vendor_id(device, vendor_id);
/* TODO: the ufshci controller could really be on any bus.. */
ufshci_parent = fu_udev_device_get_parent_with_subsystem(FU_UDEV_DEVICE(device), "pci");
if (ufshci_parent != NULL) {
guint64 ufs_features = 0;
/* check if this is a UFS device */
if (fu_udev_device_get_sysfs_attr_uint64(ufshci_parent,
"device_descriptor/ufs_features",
&ufs_features,
NULL)) {
fu_device_set_summary(device, "UFS device");
/* least significant bit specifies FFU capability */
if (ufs_features & 0x1) {
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_protocol(device, "org.jedec.ufs");
}
if (!fu_udev_device_get_sysfs_attr_uint64(ufshci_parent,
"device_descriptor/ffu_timeout",
&self->ffu_timeout,
error)) {
g_prefix_error(error, "no ffu timeout specified: ");
return FALSE;
}
}
}
/* add GUIDs */
fu_device_add_instance_strsafe(device, "VEN", fu_device_get_vendor(device));
fu_device_add_instance_strsafe(device, "DEV", fu_device_get_name(device));
fu_device_add_instance_strsafe(device, "REV", fu_device_get_version(device));
if (!fu_device_build_instance_id_quirk(device, error, "SCSI", "VEN", NULL))
return FALSE;
if (!fu_device_build_instance_id(device, error, "SCSI", "VEN", "DEV", NULL))
return FALSE;
if (!fu_device_build_instance_id(device, error, "SCSI", "VEN", "DEV", "REV", NULL))
return FALSE;
/* is internal? */
if (fu_udev_device_get_sysfs_attr_uint64(FU_UDEV_DEVICE(device),
"removable",
&removable,
NULL)) {
if (removable == 0x0)
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL);
}
/* set the physical ID */
return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "scsi", error);
}
static FuFirmware *
fu_scsi_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
g_autoptr(FuFirmware) firmware = fu_firmware_new();
fu_firmware_set_alignment(firmware, FU_FIRMWARE_ALIGNMENT_4K);
if (!fu_firmware_parse(firmware, fw, flags, error))
return NULL;
return g_steal_pointer(&firmware);
}
static const gchar *
fu_scsi_device_sense_key_to_string(guint8 key)
{
if (key == 0x00)
return "no-sense";
if (key == 0x01)
return "recovered-error";
if (key == 0x02)
return "not-ready";
if (key == 0x03)
return "medium-error";
if (key == 0x04)
return "hardware-error";
if (key == 0x05)
return "illegal-request";
if (key == 0x06)
return "unit-attention";
if (key == 0x07)
return "data-protect";
if (key == 0x08)
return "blank-check";
if (key == 0x09)
return "vendor-specific";
if (key == 0x0A)
return "copy-aborted";
if (key == 0x0B)
return "aborted-command";
if (key == 0x0C)
return "equal";
if (key == 0x0D)
return "volume-overflow";
if (key == 0x0E)
return "miscompare";
return NULL;
}
static gboolean
fu_scsi_device_send_scsi_cmd_v3(FuScsiDevice *self,
const guint8 *cdb,
guint8 cdbsz,
const guint8 *buf,
guint32 bufsz,
gint dir,
GError **error)
{
guint8 sense_buffer[SENSE_BUFF_LEN] = {0};
struct sg_io_hdr io_hdr = {.interface_id = 'S'};
io_hdr.cmd_len = cdbsz;
io_hdr.mx_sb_len = sizeof(sense_buffer);
io_hdr.dxfer_direction = dir;
io_hdr.dxfer_len = bufsz;
io_hdr.dxferp = (guint8 *)buf;
/* pointer to command buf */
io_hdr.cmdp = (guint8 *)cdb;
io_hdr.sbp = sense_buffer;
io_hdr.timeout = 60000; /* ms */
if (g_getenv("FWUPD_SCSI_VERBOSE") != NULL)
g_debug("cmd=0x%x len=0x%x", cdb[0], (guint)bufsz);
if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
SG_IO,
(guint8 *)&io_hdr,
NULL,
FU_SCSI_DEVICE_IOCTL_TIMEOUT,
error))
return FALSE;
if (io_hdr.status) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Command fail with status %x, senseKey %s, asc 0x%02x, ascq 0x%02x",
io_hdr.status,
fu_scsi_device_sense_key_to_string(sense_buffer[2]),
sense_buffer[12],
sense_buffer[13]);
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_scsi_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FuProgress *progress,
FwupdInstallFlags flags,
GError **error)
{
FuScsiDevice *self = FU_SCSI_DEVICE(device);
guint32 chunksz = 0x1000;
guint32 offset = 0;
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;
/* prepare chunks */
chunks = fu_chunk_array_new_from_bytes(fw, 0x00, 0x00, chunksz);
fu_progress_set_id(progress, G_STRLOC);
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
fu_progress_set_steps(progress, chunks->len);
/* write each block */
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index(chunks, i);
guint8 cdb[WRITE_BUF_CMDLEN] = {WRITE_BUFFER_CMD,
BUFFER_FFU_MODE,
0x0 /* buf_id */};
fu_common_write_uint24(cdb + 3, offset, G_BIG_ENDIAN);
fu_common_write_uint24(cdb + 6, fu_chunk_get_data_sz(chk), G_BIG_ENDIAN);
if (!fu_scsi_device_send_scsi_cmd_v3(self,
cdb,
sizeof(cdb),
fu_chunk_get_data(chk),
fu_chunk_get_data_sz(chk),
SG_DXFER_TO_DEV,
error)) {
g_prefix_error(error,
"SG_IO WRITE BUFFER data error for v3 chunk 0x%x: ",
fu_chunk_get_idx(chk));
return FALSE;
}
/* chunk done */
fu_progress_step_done(progress);
offset += fu_chunk_get_data_sz(chk);
}
/* success! */
fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING);
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
return TRUE;
}
static void
fu_scsi_device_set_progress(FuDevice *self, FuProgress *progress)
{
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99); /* write */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1); /* reload */
}
static void
fu_scsi_device_init(FuScsiDevice *self)
{
fu_device_add_icon(FU_DEVICE(self), "drive-harddisk");
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN);
fu_device_set_summary(FU_DEVICE(self), "SCSI device");
fu_udev_device_set_flags(FU_UDEV_DEVICE(self),
FU_UDEV_DEVICE_FLAG_OPEN_READ | FU_UDEV_DEVICE_FLAG_OPEN_SYNC |
FU_UDEV_DEVICE_FLAG_IOCTL_RETRY);
}
static void
fu_scsi_device_class_init(FuScsiDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
klass_device->to_string = fu_scsi_device_to_string;
klass_device->probe = fu_scsi_device_probe;
klass_device->prepare_firmware = fu_scsi_device_prepare_firmware;
klass_device->write_firmware = fu_scsi_device_write_firmware;
klass_device->set_progress = fu_scsi_device_set_progress;
}