fwupd/plugins/bcm57xx/fu-bcm57xx-device.c
2021-08-24 11:18:40 -05:00

658 lines
19 KiB
C

/*
* Copyright (C) 2018 Evan Lojewski
* Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: GPL-2+
*/
#include "config.h"
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <glib/gstdio.h>
#include <string.h>
#ifdef HAVE_ETHTOOL_H
#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <net/if.h>
#endif
#ifdef HAVE_IOCTL_H
#include <sys/ioctl.h>
#endif
#ifdef HAVE_SOCKET_H
#include <sys/socket.h>
#endif
#include <fwupdplugin.h>
#include "fu-bcm57xx-common.h"
#include "fu-bcm57xx-device.h"
#include "fu-bcm57xx-dict-image.h"
#include "fu-bcm57xx-firmware.h"
#include "fu-bcm57xx-recovery-device.h"
#define FU_BCM57XX_BLOCK_SZ 0x4000 /* 16kb */
struct _FuBcm57xxDevice {
FuUdevDevice parent_instance;
FuBcm57xxRecoveryDevice *recovery;
gchar *ethtool_iface;
int ethtool_fd;
};
G_DEFINE_TYPE(FuBcm57xxDevice, fu_bcm57xx_device, FU_TYPE_UDEV_DEVICE)
static void
fu_bcm57xx_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
FU_DEVICE_CLASS(fu_bcm57xx_device_parent_class)->to_string(device, idt, str);
fu_common_string_append_kv(str, idt, "EthtoolIface", self->ethtool_iface);
}
static gboolean
fu_bcm57xx_device_probe(FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
g_autofree gchar *fn = NULL;
g_autoptr(GPtrArray) ifaces = NULL;
/* FuUdevDevice->probe */
if (!FU_DEVICE_CLASS(fu_bcm57xx_device_parent_class)->probe(device, error))
return FALSE;
/* only enumerate number 0 */
if (fu_udev_device_get_number(FU_UDEV_DEVICE(device)) != 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"only device 0 supported on multi-device card");
return FALSE;
}
/* we need this even for non-recovery to reset APE */
fu_device_set_context(FU_DEVICE(self->recovery), fu_device_get_context(FU_DEVICE(self)));
fu_device_incorporate(FU_DEVICE(self->recovery), FU_DEVICE(self));
if (!fu_device_probe(FU_DEVICE(self->recovery), error))
return FALSE;
/* only if has an interface */
fn = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), "net", NULL);
if (!g_file_test(fn, G_FILE_TEST_EXISTS)) {
g_debug("waiting for net devices to appear");
g_usleep(50 * 1000);
}
ifaces = fu_common_filename_glob(fn, "en*", NULL);
if (ifaces == NULL || ifaces->len == 0) {
fu_device_add_child(FU_DEVICE(self), FU_DEVICE(self->recovery));
} else {
self->ethtool_iface = g_path_get_basename(g_ptr_array_index(ifaces, 0));
}
/* success */
return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "pci", error);
}
static gboolean
fu_bcm57xx_device_nvram_write(FuBcm57xxDevice *self,
guint32 address,
const guint8 *buf,
gsize bufsz,
GError **error)
{
#ifdef HAVE_ETHTOOL_H
gsize eepromsz;
gint rc = -1;
struct ifreq ifr = {0};
g_autofree struct ethtool_eeprom *eeprom = NULL;
/* failed to load tg3 */
if (self->ethtool_iface == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as ethtool interface disabled");
return FALSE;
}
/* sanity check */
if (address + bufsz > fu_device_get_firmware_size_max(FU_DEVICE(self))) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"tried to read outside of EEPROM size [0x%x]",
(guint)fu_device_get_firmware_size_max(FU_DEVICE(self)));
return FALSE;
}
/* write EEPROM (NVRAM) data */
eepromsz = sizeof(struct ethtool_eeprom) + bufsz;
eeprom = (struct ethtool_eeprom *)g_malloc0(eepromsz);
eeprom->cmd = ETHTOOL_SEEPROM;
eeprom->magic = BCM_NVRAM_MAGIC;
eeprom->len = bufsz;
eeprom->offset = address;
memcpy(eeprom->data, buf, eeprom->len);
strncpy(ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1);
ifr.ifr_data = (char *)eeprom;
#ifdef HAVE_IOCTL_H
rc = ioctl(self->ethtool_fd, SIOCETHTOOL, &ifr);
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
if (rc < 0) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "cannot write eeprom [%i]", rc);
return FALSE;
}
/* success */
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <linux/ethtool.h> not found");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_nvram_read(FuBcm57xxDevice *self,
guint32 address,
guint8 *buf,
gsize bufsz,
GError **error)
{
#ifdef HAVE_ETHTOOL_H
gsize eepromsz;
gint rc = -1;
struct ifreq ifr = {0};
g_autofree struct ethtool_eeprom *eeprom = NULL;
/* failed to load tg3 */
if (self->ethtool_iface == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as ethtool interface disabled");
return FALSE;
}
/* sanity check */
if (address + bufsz > fu_device_get_firmware_size_max(FU_DEVICE(self))) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"tried to read outside of EEPROM size [0x%x]",
(guint)fu_device_get_firmware_size_max(FU_DEVICE(self)));
return FALSE;
}
/* read EEPROM (NVRAM) data */
eepromsz = sizeof(struct ethtool_eeprom) + bufsz;
eeprom = (struct ethtool_eeprom *)g_malloc0(eepromsz);
eeprom->cmd = ETHTOOL_GEEPROM;
eeprom->len = bufsz;
eeprom->offset = address;
strncpy(ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1);
ifr.ifr_data = (char *)eeprom;
#ifdef HAVE_IOCTL_H
rc = ioctl(self->ethtool_fd, SIOCETHTOOL, &ifr);
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
if (rc < 0) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "cannot read eeprom [%i]", rc);
return FALSE;
}
/* copy back data */
if (!fu_memcpy_safe(buf,
bufsz,
0x0, /* dst */
(guint8 *)eeprom,
eepromsz, /* src */
G_STRUCT_OFFSET(struct ethtool_eeprom, data),
bufsz,
error))
return FALSE;
/* success */
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <linux/ethtool.h> not found");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_nvram_check(FuBcm57xxDevice *self, GError **error)
{
#ifdef HAVE_ETHTOOL_H
gint rc = -1;
struct ethtool_drvinfo drvinfo = {0};
struct ifreq ifr = {0};
/* failed to load tg3 */
if (self->ethtool_iface == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as ethtool interface disabled");
return FALSE;
}
/* get driver info */
drvinfo.cmd = ETHTOOL_GDRVINFO;
strncpy(ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1);
ifr.ifr_data = (char *)&drvinfo;
#ifdef HAVE_IOCTL_H
rc = ioctl(self->ethtool_fd, SIOCETHTOOL, &ifr);
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
if (rc < 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"cannot get driver information [%i]",
rc);
return FALSE;
}
g_debug("FW version %s", drvinfo.fw_version);
/* sanity check */
if (drvinfo.eedump_len != fu_device_get_firmware_size_max(FU_DEVICE(self))) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"EEPROM size invalid, got 0x%x, expected 0x%x",
drvinfo.eedump_len,
(guint)fu_device_get_firmware_size_max(FU_DEVICE(self)));
return FALSE;
}
/* success */
return TRUE;
#else
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <linux/ethtool.h> not found");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_activate(FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
g_autoptr(FuDeviceLocker) locker1 = NULL;
g_autoptr(FuDeviceLocker) locker2 = NULL;
/* the only way to do this is using the mmap method */
locker2 = fu_device_locker_new_full(FU_DEVICE(self->recovery),
(FuDeviceLockerFunc)fu_device_detach,
(FuDeviceLockerFunc)fu_device_attach,
error);
if (locker2 == NULL)
return FALSE;
/* open */
locker1 = fu_device_locker_new(FU_DEVICE(self->recovery), error);
if (locker1 == NULL)
return FALSE;
/* activate, causing APE reset, then close, then attach */
if (!fu_device_activate(FU_DEVICE(self->recovery), error))
return FALSE;
/* ensure we attach before we close */
if (!fu_device_locker_close(locker2, error))
return FALSE;
/* wait for the device to restart before calling reload() */
fu_device_set_status(device, FWUPD_STATUS_DEVICE_BUSY);
fu_device_sleep_with_progress(device, 5); /* seconds */
return TRUE;
}
static GBytes *
fu_bcm57xx_device_dump_firmware(FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
const gsize bufsz = fu_device_get_firmware_size_max(FU_DEVICE(self));
g_autofree guint8 *buf = g_malloc0(bufsz);
g_autoptr(GPtrArray) chunks = NULL;
fu_device_set_status(device, FWUPD_STATUS_DEVICE_READ);
chunks = fu_chunk_array_mutable_new(buf, bufsz, 0x0, 0x0, FU_BCM57XX_BLOCK_SZ);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index(chunks, i);
if (!fu_bcm57xx_device_nvram_read(self,
fu_chunk_get_address(chk),
fu_chunk_get_data_out(chk),
fu_chunk_get_data_sz(chk),
error))
return NULL;
fu_device_set_progress_full(device, i, chunks->len - 1);
}
/* read from hardware */
return g_bytes_new_take(g_steal_pointer(&buf), bufsz);
}
static FuFirmware *
fu_bcm57xx_device_read_firmware(FuDevice *device, GError **error)
{
g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new();
g_autoptr(GBytes) fw = NULL;
/* read from hardware */
fw = fu_bcm57xx_device_dump_firmware(device, error);
if (fw == NULL)
return NULL;
if (!fu_firmware_parse(firmware, fw, FWUPD_INSTALL_FLAG_NONE, error))
return NULL;
/* remove images that will contain user-data */
if (!fu_firmware_remove_image_by_id(firmware, "info", error))
return NULL;
if (!fu_firmware_remove_image_by_id(firmware, "info2", error))
return NULL;
if (!fu_firmware_remove_image_by_id(firmware, "vpd", error))
return NULL;
return g_steal_pointer(&firmware);
}
static FuFirmware *
fu_bcm57xx_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
guint dict_cnt = 0;
g_autoptr(GBytes) fw_old = NULL;
g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new();
g_autoptr(FuFirmware) firmware_tmp = fu_bcm57xx_firmware_new();
g_autoptr(FuFirmware) img_ape = NULL;
g_autoptr(FuFirmware) img_stage1 = NULL;
g_autoptr(FuFirmware) img_stage2 = NULL;
g_autoptr(GPtrArray) images = NULL;
/* try to parse NVRAM, stage1 or APE */
if (!fu_firmware_parse(firmware_tmp, fw, flags, error)) {
g_prefix_error(error, "failed to parse new firmware: ");
return NULL;
}
/* for full NVRAM image, verify if correct device */
if ((flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) == 0) {
guint16 vid = fu_bcm57xx_firmware_get_vendor(FU_BCM57XX_FIRMWARE(firmware_tmp));
guint16 did = fu_bcm57xx_firmware_get_model(FU_BCM57XX_FIRMWARE(firmware_tmp));
if (vid != 0x0 && did != 0x0 &&
(fu_udev_device_get_vendor(FU_UDEV_DEVICE(device)) != vid ||
fu_udev_device_get_model(FU_UDEV_DEVICE(device)) != did)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"PCI vendor or model incorrect, "
"got: %04X:%04X expected %04X:%04X",
vid,
did,
fu_udev_device_get_vendor(FU_UDEV_DEVICE(device)),
fu_udev_device_get_model(FU_UDEV_DEVICE(device)));
return NULL;
}
}
/* get the existing firmware from the device */
fw_old = fu_bcm57xx_device_dump_firmware(device, error);
if (fw_old == NULL)
return NULL;
if (!fu_firmware_parse(firmware, fw_old, flags, error)) {
g_prefix_error(error, "failed to parse existing firmware: ");
return NULL;
}
if (g_getenv("FWUPD_BCM57XX_VERBOSE") != NULL) {
g_autofree gchar *str = fu_firmware_to_string(firmware);
g_debug("existing device firmware: %s", str);
}
/* merge in all the provided images into the existing firmware */
img_stage1 = fu_firmware_get_image_by_id(firmware_tmp, "stage1", NULL);
if (img_stage1 != NULL)
fu_firmware_add_image(firmware, img_stage1);
img_stage2 = fu_firmware_get_image_by_id(firmware_tmp, "stage2", NULL);
if (img_stage2 != NULL)
fu_firmware_add_image(firmware, img_stage2);
img_ape = fu_firmware_get_image_by_id(firmware_tmp, "ape", NULL);
if (img_ape != NULL)
fu_firmware_add_image(firmware, img_ape);
/* the src and dst dictionaries may be in different order */
images = fu_firmware_get_images(firmware);
for (guint i = 0; i < images->len; i++) {
FuFirmware *img = g_ptr_array_index(images, i);
if (FU_IS_BCM57XX_DICT_IMAGE(img)) {
fu_firmware_set_idx(img, 0x80 + dict_cnt);
dict_cnt++;
}
}
if (g_getenv("FWUPD_BCM57XX_VERBOSE") != NULL) {
g_autofree gchar *str = fu_firmware_to_string(firmware);
g_debug("proposed device firmware: %s", str);
}
/* success */
return g_steal_pointer(&firmware);
}
static gboolean
fu_bcm57xx_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
g_autoptr(GBytes) blob = NULL;
g_autoptr(GBytes) blob_verify = NULL;
g_autoptr(GPtrArray) chunks = NULL;
/* build the images into one linear blob of the correct size */
fu_device_set_status(device, FWUPD_STATUS_DECOMPRESSING);
blob = fu_firmware_write(firmware, error);
if (blob == NULL)
return FALSE;
/* hit hardware */
fu_device_set_status(device, FWUPD_STATUS_DEVICE_WRITE);
chunks = fu_chunk_array_new_from_bytes(blob, 0x0, 0x0, FU_BCM57XX_BLOCK_SZ);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index(chunks, i);
if (!fu_bcm57xx_device_nvram_write(self,
fu_chunk_get_address(chk),
fu_chunk_get_data(chk),
fu_chunk_get_data_sz(chk),
error))
return FALSE;
fu_device_set_progress_full(device, i, chunks->len - 1);
}
/* verify */
fu_device_set_status(device, FWUPD_STATUS_DEVICE_VERIFY);
blob_verify = fu_bcm57xx_device_dump_firmware(device, error);
if (blob_verify == NULL)
return FALSE;
if (!fu_common_bytes_compare(blob, blob_verify, error))
return FALSE;
/* reset APE */
return fu_device_activate(device, error);
}
static gboolean
fu_bcm57xx_device_setup(FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
guint32 fwversion = 0;
/* device is in recovery mode */
if (self->ethtool_iface == NULL) {
g_autoptr(FuDeviceLocker) locker = NULL;
g_debug("device in recovery mode, use alternate device");
locker = fu_device_locker_new(FU_DEVICE(self->recovery), error);
if (locker == NULL)
return FALSE;
return fu_device_setup(FU_DEVICE(self->recovery), error);
}
/* check the EEPROM size */
if (!fu_bcm57xx_device_nvram_check(self, error))
return FALSE;
/* get NVRAM version */
if (!fu_bcm57xx_device_nvram_read(self,
BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERSION,
(guint8 *)&fwversion,
sizeof(guint32),
error))
return FALSE;
if (fwversion != 0x0) {
g_autofree gchar *fwversion_str = NULL;
/* this is only set on the OSS firmware */
fwversion_str = fu_common_version_from_uint32(GUINT32_FROM_BE(fwversion),
FWUPD_VERSION_FORMAT_TRIPLET);
fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET);
fu_device_set_version(device, fwversion_str);
fu_device_set_version_raw(device, fwversion);
fu_device_set_branch(device, BCM_FW_BRANCH_OSS_FIRMWARE);
} else {
guint8 bufver[16] = {0x0};
guint32 veraddr = 0;
g_autoptr(Bcm57xxVeritem) veritem = NULL;
/* fall back to the string, e.g. '5719-v1.43' */
if (!fu_bcm57xx_device_nvram_read(self,
BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERADDR,
(guint8 *)&veraddr,
sizeof(guint32),
error))
return FALSE;
veraddr = GUINT32_FROM_BE(veraddr);
if (veraddr > BCM_PHYS_ADDR_DEFAULT)
veraddr -= BCM_PHYS_ADDR_DEFAULT;
if (!fu_bcm57xx_device_nvram_read(self,
BCM_NVRAM_STAGE1_BASE + veraddr,
bufver,
sizeof(bufver),
error))
return FALSE;
veritem = fu_bcm57xx_veritem_new(bufver, sizeof(bufver));
if (veritem != NULL) {
fu_device_set_version_format(device, veritem->verfmt);
fu_device_set_version(device, veritem->version);
fu_device_set_branch(device, veritem->branch);
}
}
/* success */
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL);
return TRUE;
}
static gboolean
fu_bcm57xx_device_open(FuDevice *device, GError **error)
{
#ifdef HAVE_SOCKET_H
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
self->ethtool_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (self->ethtool_fd < 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"failed to open socket: %s",
#ifdef HAVE_ERRNO_H
strerror(errno));
#else
"unspecified error");
#endif
return FALSE;
}
return TRUE;
#else
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"socket() not supported as sys/socket.h not available");
return FALSE;
#endif
}
static gboolean
fu_bcm57xx_device_close(FuDevice *device, GError **error)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device);
return g_close(self->ethtool_fd, error);
}
static void
fu_bcm57xx_device_init(FuBcm57xxDevice *self)
{
fu_device_add_protocol(FU_DEVICE(self), "com.broadcom.bcm57xx");
fu_device_add_icon(FU_DEVICE(self), "network-wired");
/* other values are set from a quirk */
fu_device_set_firmware_size(FU_DEVICE(self), BCM_FIRMWARE_SIZE);
/* used for recovery in case of ethtool failure and for APE reset */
self->recovery = fu_bcm57xx_recovery_device_new();
}
static void
fu_bcm57xx_device_finalize(GObject *object)
{
FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(object);
g_free(self->ethtool_iface);
G_OBJECT_CLASS(fu_bcm57xx_device_parent_class)->finalize(object);
}
static void
fu_bcm57xx_device_class_init(FuBcm57xxDeviceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
object_class->finalize = fu_bcm57xx_device_finalize;
klass_device->prepare_firmware = fu_bcm57xx_device_prepare_firmware;
klass_device->setup = fu_bcm57xx_device_setup;
klass_device->reload = fu_bcm57xx_device_setup;
klass_device->open = fu_bcm57xx_device_open;
klass_device->close = fu_bcm57xx_device_close;
klass_device->activate = fu_bcm57xx_device_activate;
klass_device->write_firmware = fu_bcm57xx_device_write_firmware;
klass_device->read_firmware = fu_bcm57xx_device_read_firmware;
klass_device->dump_firmware = fu_bcm57xx_device_dump_firmware;
klass_device->probe = fu_bcm57xx_device_probe;
klass_device->to_string = fu_bcm57xx_device_to_string;
}