/* * Copyright (C) 2018-2020 Evan Lojewski * Copyright (C) 2020 Richard Hughes * * SPDX-License-Identifier: GPL-2+ */ #include "config.h" #ifdef HAVE_ERRNO_H #include #endif #include #ifdef HAVE_ETHTOOL_H #include #include #include #endif #ifdef HAVE_IOCTL_H #include #endif #ifdef HAVE_SOCKET_H #include #endif #include "fu-chunk.h" #include "fu-common.h" #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-device.h" #include "fu-bcm57xx-recovery-device.h" #include "fu-bcm57xx-firmware.h" #include "fu-bcm57xx-dict-image.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 (FuUdevDevice *device, guint idt, GString *str) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device); fu_common_string_append_kv (str, idt, "EthtoolIface", self->ethtool_iface); } static gboolean fu_bcm57xx_device_probe (FuUdevDevice *device, GError **error) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE (device); g_autofree gchar *fn = NULL; g_autoptr(GPtrArray) ifaces = NULL; /* only enumerate number 0 */ if (fu_udev_device_get_number (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_quirks (FU_DEVICE (self->recovery), fu_device_get_quirks (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 (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 (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 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 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 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 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 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 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_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, chk->address, (guint8 *) chk->data, chk->data_sz, 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(FuFirmwareImage) img_ape = NULL; g_autoptr(FuFirmwareImage) img_stage1 = NULL; g_autoptr(FuFirmwareImage) 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 firware: %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++) { FuFirmwareImage *img = g_ptr_array_index (images, i); if (FU_IS_BCM57XX_DICT_IMAGE (img)) { fu_firmware_image_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 firware: %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, chk->address, chk->data, chk->data_sz, 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_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_NO_GUID_MATCHING); fu_device_set_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); FuUdevDeviceClass *klass_udev_device = FU_UDEV_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_udev_device->probe = fu_bcm57xx_device_probe; klass_udev_device->to_string = fu_bcm57xx_device_to_string; }