fwupd/plugins/emmc/fu-emmc-device.c
Daniil Lunev 0af67e3e50 emmc: Filter out rpmb and boot block devices
The move to query sysattr for the name inadvertly changed the extent of
devices accepted by emmc probe. udev rules setup ID_NAME only for main
block device, but not for rpmb or boot, while 'name' sysattr is present
for all of them for they point to the same parent. The original check
for name was brittle either way and relied on a side effect of udev
subsystem. This CL introdues a proper filtering check that picks up only
the main devices. That is important because an attempt to perfrom an
update not on the main device returns a cryptic timeout error.

Change-Id: I3c2f5597c3e3ca2124c87cade3f345dece94027e
2021-07-14 07:38:45 +01:00

539 lines
16 KiB
C

/*
* Copyright (C) 2019 Mario Limonciello <mario.limonciello@dell.com>
*
* SPDX-License-Identifier: GPL-2+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <sys/ioctl.h>
#include <linux/mmc/ioctl.h>
#include "fu-emmc-device.h"
/* From kernel linux/major.h */
#define MMC_BLOCK_MAJOR 179
/* From kernel linux/mmc/mmc.h */
#define MMC_SWITCH 6 /* ac [31:0] See below R1b */
#define MMC_SEND_EXT_CSD 8 /* adtc R1 */
#define MMC_SWITCH_MODE_WRITE_BYTE 0x03 /* Set target to value */
#define MMC_WRITE_BLOCK 24 /* adtc [31:0] data addr R1 */
/* From kernel linux/mmc/core.h */
#define MMC_RSP_PRESENT (1 << 0)
#define MMC_RSP_CRC (1 << 2) /* expect valid crc */
#define MMC_RSP_BUSY (1 << 3) /* card may send busy */
#define MMC_RSP_OPCODE (1 << 4) /* response contains opcode */
#define MMC_RSP_SPI_S1 (1 << 7) /* one status byte */
#define MMC_CMD_AC (0 << 5)
#define MMC_CMD_ADTC (1 << 5)
#define MMC_RSP_SPI_BUSY (1 << 10) /* card may send busy */
#define MMC_RSP_SPI_R1 (MMC_RSP_SPI_S1)
#define MMC_RSP_SPI_R1B (MMC_RSP_SPI_S1|MMC_RSP_SPI_BUSY)
#define MMC_RSP_R1 (MMC_RSP_PRESENT|MMC_RSP_CRC|MMC_RSP_OPCODE)
#define MMC_RSP_R1B (MMC_RSP_PRESENT|MMC_RSP_CRC|MMC_RSP_OPCODE|MMC_RSP_BUSY)
/* EXT_CSD fields */
#define EXT_CSD_SUPPORTED_MODES 493 /* RO */
#define EXT_CSD_FFU_FEATURES 492 /* RO */
#define EXT_CSD_FFU_ARG_3 490 /* RO */
#define EXT_CSD_FFU_ARG_2 489 /* RO */
#define EXT_CSD_FFU_ARG_1 488 /* RO */
#define EXT_CSD_FFU_ARG_0 487 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_3 305 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_2 304 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_1 303 /* RO */
#define EXT_CSD_NUM_OF_FW_SEC_PROG_0 302 /* RO */
#define EXT_CSD_REV 192
#define EXT_CSD_FW_CONFIG 169 /* R/W */
#define EXT_CSD_DATA_SECTOR_SIZE 61 /* R */
#define EXT_CSD_MODE_CONFIG 30
#define EXT_CSD_MODE_OPERATION_CODES 29 /* W */
#define EXT_CSD_FFU_STATUS 26 /* R */
#define EXT_CSD_REV_V5_1 8
#define EXT_CSD_REV_V5_0 7
/* EXT_CSD field definitions */
#define EXT_CSD_NORMAL_MODE (0x00)
#define EXT_CSD_FFU_MODE (0x01)
#define EXT_CSD_FFU_INSTALL (0x01)
#define EXT_CSD_FFU (1<<0)
#define EXT_CSD_UPDATE_DISABLE (1<<0)
#define EXT_CSD_CMD_SET_NORMAL (1<<0)
struct _FuEmmcDevice {
FuUdevDevice parent_instance;
guint32 sect_size;
};
G_DEFINE_TYPE (FuEmmcDevice, fu_emmc_device, FU_TYPE_UDEV_DEVICE)
static void
fu_emmc_device_to_string (FuDevice *device, guint idt, GString *str)
{
FuEmmcDevice *self = FU_EMMC_DEVICE (device);
FU_DEVICE_CLASS (fu_emmc_device_parent_class)->to_string (device, idt, str);
fu_common_string_append_ku (str, idt, "SectorSize", self->sect_size);
}
static const gchar *
fu_emmc_device_get_manufacturer (guint64 mmc_id)
{
switch (mmc_id) {
case 0x00:
case 0x44:
return "SanDisk";
case 0x02:
return "Kingston/Sandisk";
case 0x03:
case 0x11:
return "Toshiba";
case 0x13:
return "Micron";
case 0x15:
return "Samsung/Sandisk/LG";
case 0x37:
return "Kingmax";
case 0x70:
case 0x2c:
return "Kingston";
default:
return NULL;
}
return NULL;
}
static gboolean
fu_emmc_device_get_sysattr_guint64 (GUdevDevice *device,
const gchar *name,
guint64 *val_out,
GError **error)
{
const gchar *sysfs;
sysfs = g_udev_device_get_sysfs_attr (device, name);
if (sysfs == NULL) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"failed get %s for %s", name, sysfs);
return FALSE;
}
*val_out = g_ascii_strtoull (sysfs, NULL, 16);
return TRUE;
}
static gboolean
fu_emmc_device_probe (FuDevice *device, GError **error)
{
GUdevDevice *udev_device = fu_udev_device_get_dev (FU_UDEV_DEVICE (device));
guint64 flag;
guint64 oemid = 0;
guint64 manfid = 0;
const gchar *tmp;
g_autoptr(GUdevDevice) udev_parent = NULL;
g_autofree gchar *name_only = NULL;
g_autofree gchar *man_oem = NULL;
g_autofree gchar *man_oem_name = NULL;
g_autofree gchar *vendor_id = NULL;
g_autoptr(GRegex) dev_regex = NULL;
/* FuUdevDevice->probe */
if (!FU_DEVICE_CLASS (fu_emmc_device_parent_class)->probe (device, error))
return FALSE;
udev_parent = g_udev_device_get_parent_with_subsystem (udev_device, "mmc", NULL);
if (udev_parent == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"no MMC parent");
return FALSE;
}
/* look for only the parent node */
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;
}
/* ignore *rpmb and *boot* mmc block devices */
dev_regex = g_regex_new ("mmcblk\\d$", 0, 0, NULL);
tmp = g_udev_device_get_name (udev_device);
if (tmp == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"device has no name");
return FALSE;
}
if (!g_regex_match (dev_regex, tmp, 0, NULL)) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"is not raw mmc block device, devname=%s",
g_udev_device_get_name (udev_device));
return FALSE;
}
/* doesn't support FFU */
if (!fu_emmc_device_get_sysattr_guint64 (udev_parent, "ffu_capable", &flag, error))
return FALSE;
if (flag == 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"%s does not support field firmware updates",
fu_device_get_name (device));
return FALSE;
}
/* name */
tmp = g_udev_device_get_sysfs_attr (udev_parent, "name");
if (tmp == NULL) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"%s does not have 'name' sysattr",
fu_device_get_name (device));
return FALSE;
}
fu_device_set_name (device, tmp);
name_only = g_strdup_printf ("EMMC\\%s", fu_device_get_name (device));
fu_device_add_instance_id (device, name_only);
/* manfid + oemid, manfid + oemid + name */
if (!fu_emmc_device_get_sysattr_guint64 (udev_parent, "manfid", &manfid, error))
return FALSE;
if (!fu_emmc_device_get_sysattr_guint64 (udev_parent, "oemid", &oemid, error))
return FALSE;
man_oem = g_strdup_printf ("EMMC\\%04" G_GUINT64_FORMAT "&%04" G_GUINT64_FORMAT,
manfid, oemid);
fu_device_add_instance_id (device, man_oem);
man_oem_name = g_strdup_printf ("EMMC\\%04" G_GUINT64_FORMAT "&%04" G_GUINT64_FORMAT "&%s",
manfid, oemid, fu_device_get_name (device));
fu_device_add_instance_id (device, man_oem_name);
/* set the vendor */
tmp = g_udev_device_get_sysfs_attr (udev_parent, "manfid");
vendor_id = g_strdup_printf ("EMMC:%s", tmp);
fu_device_add_vendor_id (device, vendor_id);
fu_device_set_vendor (device, fu_emmc_device_get_manufacturer (manfid));
/* set the physical ID */
if (!fu_udev_device_set_physical_id (FU_UDEV_DEVICE (device), "mmc", error))
return FALSE;
/* internal */
if (!fu_emmc_device_get_sysattr_guint64 (udev_device, "removable", &flag, error))
return FALSE;
if (flag == 0)
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_INTERNAL);
/* firmware version */
tmp = g_udev_device_get_sysfs_attr (udev_parent, "fwrev");
if (tmp != NULL) {
fu_device_set_version_format (device, FWUPD_VERSION_FORMAT_NUMBER);
fu_device_set_version (device, tmp);
}
return TRUE;
}
static gboolean
fu_emmc_read_extcsd (FuEmmcDevice *self, guint8 *ext_csd, gsize ext_csd_sz, GError **error)
{
struct mmc_ioc_cmd idata = { 0x0 };
idata.write_flag = 0;
idata.opcode = MMC_SEND_EXT_CSD;
idata.arg = 0;
idata.flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC;
idata.blksz = 512;
idata.blocks = 1;
mmc_ioc_cmd_set_data (idata, ext_csd);
return fu_udev_device_ioctl (FU_UDEV_DEVICE (self),
MMC_IOC_CMD, (guint8 *) &idata,
NULL, error);
}
static gboolean
fu_emmc_validate_extcsd (FuDevice *device, GError **error)
{
FuEmmcDevice *self = FU_EMMC_DEVICE (device);
guint8 ext_csd[512] = { 0x0 };
if (!fu_emmc_read_extcsd (FU_EMMC_DEVICE (device), ext_csd, sizeof (ext_csd), error))
return FALSE;
if (ext_csd[EXT_CSD_REV] < EXT_CSD_REV_V5_0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"FFU is only available on devices >= "
"MMC 5.0, not supported in %s",
fu_device_get_name (device));
return FALSE;
}
if ((ext_csd[EXT_CSD_SUPPORTED_MODES] & EXT_CSD_FFU) == 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"FFU is not supported in %s",
fu_device_get_name (device));
return FALSE;
}
if (ext_csd[EXT_CSD_FW_CONFIG] & EXT_CSD_UPDATE_DISABLE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"firmware update was disabled in %s",
fu_device_get_name (device));
return FALSE;
}
self->sect_size = (ext_csd[EXT_CSD_DATA_SECTOR_SIZE] == 0) ? 512 : 4096;
return TRUE;
}
static gboolean
fu_emmc_device_setup (FuDevice *device, GError **error)
{
g_autoptr(GError) error_validate = NULL;
if (!fu_emmc_validate_extcsd (device, &error_validate))
g_debug ("%s", error_validate->message);
else
fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_UPDATABLE);
return TRUE;
}
static FuFirmware *
fu_emmc_device_prepare_firmware (FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
FuEmmcDevice *self = FU_EMMC_DEVICE (device);
gsize fw_size = g_bytes_get_size (fw);
/* check alignment */
if ((fw_size % self->sect_size) > 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"firmware data size (%" G_GSIZE_FORMAT ") is not aligned", fw_size);
return NULL;
}
return fu_firmware_new_from_bytes (fw);
}
static gboolean
fu_emmc_device_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuEmmcDevice *self= FU_EMMC_DEVICE (device);
gsize fw_size = 0;
gsize total_done;
guint32 arg;
guint32 sect_done = 0;
guint8 ext_csd[512];
guint failure_cnt = 0;
g_autofree struct mmc_ioc_multi_cmd *multi_cmd = NULL;
g_autoptr(GBytes) fw = NULL;
g_autoptr(GPtrArray) chunks = NULL;
if (!fu_emmc_read_extcsd (FU_EMMC_DEVICE (device), ext_csd, sizeof (ext_csd), error))
return FALSE;
fw = fu_firmware_get_bytes (firmware, error);
if (fw == NULL)
return FALSE;
fw_size = g_bytes_get_size (fw);
/* set CMD ARG */
arg = ext_csd[EXT_CSD_FFU_ARG_0] |
ext_csd[EXT_CSD_FFU_ARG_1] << 8 |
ext_csd[EXT_CSD_FFU_ARG_2] << 16 |
ext_csd[EXT_CSD_FFU_ARG_3] << 24;
/* prepare multi_cmd to be sent */
multi_cmd = g_malloc0 (sizeof(struct mmc_ioc_multi_cmd) +
3 * sizeof(struct mmc_ioc_cmd));
multi_cmd->num_of_cmds = 3;
/* put device into ffu mode */
multi_cmd->cmds[0].opcode = MMC_SWITCH;
multi_cmd->cmds[0].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) |
(EXT_CSD_MODE_CONFIG << 16) |
(EXT_CSD_FFU_MODE << 8) |
EXT_CSD_CMD_SET_NORMAL;
multi_cmd->cmds[0].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
multi_cmd->cmds[0].write_flag = 1;
/* send image chunk */
multi_cmd->cmds[1].opcode = MMC_WRITE_BLOCK;
multi_cmd->cmds[1].blksz = self->sect_size;
multi_cmd->cmds[1].blocks = 1;
multi_cmd->cmds[1].arg = arg;
multi_cmd->cmds[1].flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC;
multi_cmd->cmds[1].write_flag = 1;
/* return device into normal mode */
multi_cmd->cmds[2].opcode = MMC_SWITCH;
multi_cmd->cmds[2].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) |
(EXT_CSD_MODE_CONFIG << 16) |
(EXT_CSD_NORMAL_MODE << 8) |
EXT_CSD_CMD_SET_NORMAL;
multi_cmd->cmds[2].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
multi_cmd->cmds[2].write_flag = 1;
/* build packets */
chunks = fu_chunk_array_new_from_bytes (fw,
0x00, /* start addr */
0x00, /* page_sz */
self->sect_size);
while (sect_done == 0) {
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index (chunks, i);
mmc_ioc_cmd_set_data (multi_cmd->cmds[1], fu_chunk_get_data (chk));
if (!fu_udev_device_ioctl (FU_UDEV_DEVICE (self),
MMC_IOC_MULTI_CMD, (guint8 *) multi_cmd,
NULL, error)) {
g_autoptr(GError) error_local = NULL;
g_prefix_error (error, "multi-cmd failed: ");
/* multi-cmd ioctl failed before exiting from ffu mode */
if (!fu_udev_device_ioctl (FU_UDEV_DEVICE (self),
MMC_IOC_CMD, (guint8 *) &multi_cmd->cmds[2],
NULL, &error_local)) {
g_prefix_error (error, "%s: ",
error_local->message);
}
return FALSE;
}
if (!fu_emmc_read_extcsd (self, ext_csd, sizeof (ext_csd), error))
return FALSE;
/* if we need to restart the download */
sect_done = ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_0] |
ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_1] << 8 |
ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_2] << 16 |
ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_3] << 24;
if (sect_done == 0) {
if (failure_cnt >= 3) {
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"programming failed");
return FALSE;
}
failure_cnt++;
g_debug ("programming failed: retrying (%u)", failure_cnt);
break;
}
/* update progress */
fu_device_set_progress_full (device, (gsize) i, (gsize) chunks->len - 1);
}
}
/* sanity check */
total_done = (gsize) sect_done * (gsize) self->sect_size;
if (total_done != fw_size) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"firmware size and number of sectors written "
"mismatch (%" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT "):",
total_done, fw_size);
return FALSE;
}
/* check mode operation for ffu install*/
if (!ext_csd[EXT_CSD_FFU_FEATURES]) {
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
} else {
/* re-enter ffu mode and install the firmware */
multi_cmd->num_of_cmds = 2;
/* set ext_csd to install mode */
multi_cmd->cmds[1].opcode = MMC_SWITCH;
multi_cmd->cmds[1].blksz = 0;
multi_cmd->cmds[1].blocks = 0;
multi_cmd->cmds[1].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) |
(EXT_CSD_MODE_OPERATION_CODES << 16) |
(EXT_CSD_FFU_INSTALL << 8) |
EXT_CSD_CMD_SET_NORMAL;
multi_cmd->cmds[1].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
multi_cmd->cmds[1].write_flag = 1;
/* send ioctl with multi-cmd */
if (!fu_udev_device_ioctl (FU_UDEV_DEVICE (self),
MMC_IOC_MULTI_CMD, (guint8 *) multi_cmd,
NULL, error)) {
g_autoptr(GError) error_local = NULL;
/* In case multi-cmd ioctl failed before exiting from ffu mode */
g_prefix_error (error, "multi-cmd failed setting install mode: ");
if (!fu_udev_device_ioctl (FU_UDEV_DEVICE (self),
MMC_IOC_CMD, (guint8 *) &multi_cmd->cmds[2],
NULL, &error_local)) {
g_prefix_error (error, "%s: ",
error_local->message);
}
return FALSE;
}
/* return status */
if (!fu_emmc_read_extcsd (self, ext_csd, sizeof (ext_csd), error))
return FALSE;
if (ext_csd[EXT_CSD_FFU_STATUS] != 0) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"FFU install failed: %d",
ext_csd[EXT_CSD_FFU_STATUS]);
return FALSE;
}
}
return TRUE;
}
static void
fu_emmc_device_init (FuEmmcDevice *self)
{
fu_device_add_protocol (FU_DEVICE (self), "org.jedec.mmc");
fu_device_add_icon (FU_DEVICE (self), "media-memory");
}
static void
fu_emmc_device_finalize (GObject *object)
{
G_OBJECT_CLASS (fu_emmc_device_parent_class)->finalize (object);
}
static void
fu_emmc_device_class_init (FuEmmcDeviceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
object_class->finalize = fu_emmc_device_finalize;
klass_device->setup = fu_emmc_device_setup;
klass_device->to_string = fu_emmc_device_to_string;
klass_device->prepare_firmware = fu_emmc_device_prepare_firmware;
klass_device->probe = fu_emmc_device_probe;
klass_device->write_firmware = fu_emmc_device_write_firmware;
}