fwupd/plugins/thunderbolt/fu-thunderbolt-device.c
Richard Hughes 99df74f0c2 Add API to wait for a device
This allows us to ignore all the delays when the device is emulated, with the
idea being to do dozens of device emulations in the CI tests.

Also, do not call fu_progress_sleep() when the device is emulated.
2023-02-01 09:42:08 +00:00

447 lines
13 KiB
C

/*
* Copyright (C) 2017 Christian J. Kellner <christian@kellner.me>
* Copyright (C) 2020 Mario Limonciello <mario.limonciello@dell.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "fu-thunderbolt-device.h"
typedef struct {
const gchar *auth_method;
} FuThunderboltDevicePrivate;
#define TBT_NVM_RETRY_TIMEOUT 200 /* ms */
#define FU_PLUGIN_THUNDERBOLT_UPDATE_TIMEOUT 60000 /* ms */
G_DEFINE_TYPE_WITH_PRIVATE(FuThunderboltDevice, fu_thunderbolt_device, FU_TYPE_UDEV_DEVICE)
#define GET_PRIVATE(o) (fu_thunderbolt_device_get_instance_private(o))
GFile *
fu_thunderbolt_device_find_nvmem(FuThunderboltDevice *self, gboolean active, GError **error)
{
const gchar *nvmem_dir = active ? "nvm_active" : "nvm_non_active";
const gchar *name;
const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self));
g_autoptr(GDir) d = NULL;
if (G_UNLIKELY(devpath == NULL)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"Could not determine sysfs path for device");
return NULL;
}
d = g_dir_open(devpath, 0, error);
if (d == NULL)
return NULL;
while ((name = g_dir_read_name(d)) != NULL) {
if (g_str_has_prefix(name, nvmem_dir)) {
g_autoptr(GFile) parent = g_file_new_for_path(devpath);
g_autoptr(GFile) nvm_dir = g_file_get_child(parent, name);
return g_file_get_child(nvm_dir, "nvmem");
}
}
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Could not find non-volatile memory location");
return NULL;
}
gboolean
fu_thunderbolt_device_check_authorized(FuThunderboltDevice *self, GError **error)
{
guint64 status;
g_autofree gchar *attribute = NULL;
const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self));
/* read directly from file to prevent udev caching */
g_autofree gchar *safe_path = g_build_path("/", devpath, "authorized", NULL);
if (!g_file_test(safe_path, G_FILE_TEST_EXISTS)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"missing authorized attribute");
return FALSE;
}
if (!g_file_get_contents(safe_path, &attribute, NULL, error))
return FALSE;
status = g_ascii_strtoull(attribute, NULL, 16);
if (status == G_MAXUINT64 && errno == ERANGE) {
g_set_error(error,
G_IO_ERROR,
g_io_error_from_errno(errno),
"failed to read 'authorized: %s",
g_strerror(errno));
return FALSE;
}
if (status == 1 || status == 2)
fu_device_uninhibit(FU_DEVICE(self), "not-authorized");
else
fu_device_inhibit(FU_DEVICE(self), "not-authorized", "Not authorized");
return TRUE;
}
gboolean
fu_thunderbolt_device_get_version(FuThunderboltDevice *self, GError **error)
{
const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self));
g_auto(GStrv) split = NULL;
g_autofree gchar *version_raw = NULL;
g_autofree gchar *version = NULL;
/* read directly from file to prevent udev caching */
g_autofree gchar *safe_path = g_build_path("/", devpath, "nvm_version", NULL);
if (!g_file_test(safe_path, G_FILE_TEST_EXISTS)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"missing nvm_version attribute");
return FALSE;
}
for (guint i = 0; i < 50; i++) {
g_autoptr(GError) error_local = NULL;
/* glib can't return a properly mapped -ENODATA but the
* kernel only returns -ENODATA or -EAGAIN */
if (g_file_get_contents(safe_path, &version_raw, NULL, &error_local))
break;
g_debug("Attempt %u: Failed to read NVM version", i);
fu_device_sleep(FU_DEVICE(self), TBT_NVM_RETRY_TIMEOUT);
/* safe mode probably */
if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
break;
}
if (version_raw == NULL) {
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to read NVM");
return FALSE;
}
split = g_strsplit(version_raw, ".", -1);
if (g_strv_length(split) != 2) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid nvm_version format: %s",
version_raw);
return FALSE;
}
version = g_strdup_printf("%02x.%02x",
(guint)g_ascii_strtoull(split[0], NULL, 16),
(guint)g_ascii_strtoull(split[1], NULL, 16));
fu_device_set_version(FU_DEVICE(self), version);
return TRUE;
}
static void
fu_thunderbolt_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device);
FuThunderboltDevicePrivate *priv = GET_PRIVATE(self);
/* FuUdevDevice->to_string */
FU_DEVICE_CLASS(fu_thunderbolt_device_parent_class)->to_string(device, idt, str);
fu_string_append(str, idt, "AuthMethod", priv->auth_method);
}
void
fu_thunderbolt_device_set_auth_method(FuThunderboltDevice *self, const gchar *auth_method)
{
FuThunderboltDevicePrivate *priv = GET_PRIVATE(self);
priv->auth_method = auth_method;
}
static gboolean
fu_thunderbolt_device_activate(FuDevice *device, FuProgress *progress, GError **error)
{
FuUdevDevice *udev = FU_UDEV_DEVICE(device);
return fu_udev_device_write_sysfs(udev, "nvm_authenticate", "1", error);
}
static gboolean
fu_thunderbolt_device_authenticate(FuDevice *device, GError **error)
{
FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device);
FuThunderboltDevicePrivate *priv = GET_PRIVATE(self);
FuUdevDevice *udev = FU_UDEV_DEVICE(device);
return fu_udev_device_write_sysfs(udev, priv->auth_method, "1", error);
}
static gboolean
fu_thunderbolt_device_flush_update(FuDevice *device, GError **error)
{
FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device);
FuThunderboltDevicePrivate *priv = GET_PRIVATE(self);
FuUdevDevice *udev = FU_UDEV_DEVICE(device);
return fu_udev_device_write_sysfs(udev, priv->auth_method, "2", error);
}
static gboolean
fu_thunderbolt_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
const gchar *attribute;
guint64 status;
/* now check if the update actually worked */
attribute =
fu_udev_device_get_sysfs_attr(FU_UDEV_DEVICE(device), "nvm_authenticate", error);
if (attribute == NULL)
return FALSE;
status = g_ascii_strtoull(attribute, NULL, 16);
if (status == G_MAXUINT64 && errno == ERANGE) {
g_set_error(error,
G_IO_ERROR,
g_io_error_from_errno(errno),
"failed to read 'nvm_authenticate: %s",
g_strerror(errno));
return FALSE;
}
/* anything else then 0x0 means we got an error */
if (status != 0x0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"update failed (status %" G_GINT64_MODIFIER "x)",
status);
return FALSE;
}
return TRUE;
}
static gboolean
fu_thunderbolt_device_rescan(FuDevice *device, GError **error)
{
FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device);
/* refresh updatability */
if (!fu_thunderbolt_device_check_authorized(self, error))
return FALSE;
/* refresh the version */
return fu_thunderbolt_device_get_version(self, error);
}
static gboolean
fu_thunderbolt_device_write_data(FuThunderboltDevice *self,
GBytes *blob_fw,
FuProgress *progress,
GError **error)
{
gsize fw_size;
gsize nwritten;
gssize n;
g_autoptr(GFile) nvmem = NULL;
g_autoptr(GOutputStream) os = NULL;
nvmem = fu_thunderbolt_device_find_nvmem(self, FALSE, error);
if (nvmem == NULL)
return FALSE;
os = (GOutputStream *)g_file_append_to(nvmem, G_FILE_CREATE_NONE, NULL, error);
if (os == NULL)
return FALSE;
nwritten = 0;
fw_size = g_bytes_get_size(blob_fw);
do {
g_autoptr(GBytes) fw_data = NULL;
fw_data = fu_bytes_new_offset(blob_fw, nwritten, fw_size - nwritten, error);
if (fw_data == NULL)
return FALSE;
n = g_output_stream_write_bytes(os, fw_data, NULL, error);
if (n < 0)
return FALSE;
nwritten += n;
fu_progress_set_percentage_full(progress, nwritten, fw_size);
} while (nwritten < fw_size);
if (nwritten != fw_size) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_WRITE,
"Could not write all data to nvmem");
return FALSE;
}
return g_output_stream_close(os, NULL, error);
}
static FuFirmware *
fu_thunderbolt_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device);
g_autoptr(FuFirmware) firmware = NULL;
g_autoptr(FuFirmware) firmware_old = NULL;
g_autoptr(GBytes) controller_fw = NULL;
g_autoptr(GFile) nvmem = NULL;
/* parse */
firmware = fu_firmware_new_from_gtypes(fw,
flags,
error,
FU_TYPE_INTEL_THUNDERBOLT_FIRMWARE,
FU_TYPE_FIRMWARE,
G_TYPE_INVALID);
if (firmware == NULL)
return NULL;
/* get current NVMEM */
nvmem = fu_thunderbolt_device_find_nvmem(self, TRUE, error);
if (nvmem == NULL)
return NULL;
controller_fw = g_file_load_bytes(nvmem, NULL, NULL, error);
if (controller_fw == NULL)
return NULL;
firmware_old = fu_firmware_new_from_gtypes(controller_fw,
flags,
error,
FU_TYPE_INTEL_THUNDERBOLT_NVM,
FU_TYPE_FIRMWARE,
G_TYPE_INVALID);
if (firmware_old == NULL)
return NULL;
if (!fu_firmware_check_compatible(firmware_old, firmware, flags, error))
return NULL;
/* success */
return g_steal_pointer(&firmware);
}
static gboolean
fu_thunderbolt_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FuProgress *progress,
FwupdInstallFlags flags,
GError **error)
{
FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device);
g_autoptr(GBytes) blob_fw = NULL;
/* get default image */
blob_fw = fu_firmware_get_bytes(firmware, error);
if (blob_fw == NULL)
return FALSE;
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
if (!fu_thunderbolt_device_write_data(self, blob_fw, progress, error)) {
g_prefix_error(error,
"could not write firmware to thunderbolt device at %s: ",
fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)));
return FALSE;
}
/* flush the image if supported by kernel and/or device */
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) {
if (!fu_thunderbolt_device_flush_update(device, error))
return FALSE;
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION);
}
/* using an active delayed activation flow later (either shutdown or another plugin) */
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SKIPS_RESTART)) {
g_debug("Skipping Thunderbolt reset per quirk request");
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION);
return TRUE;
}
/* authenticate (possibly on unplug if device supports it) */
if (!fu_thunderbolt_device_authenticate(FU_DEVICE(self), error)) {
g_prefix_error(error, "could not start thunderbolt device upgrade: ");
return FALSE;
}
/* whether to wait for a device replug or not */
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) {
fu_device_set_remove_delay(device, FU_PLUGIN_THUNDERBOLT_UPDATE_TIMEOUT);
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART);
}
return TRUE;
}
static gboolean
fu_thunderbolt_device_probe(FuDevice *device, GError **error)
{
g_autoptr(FuUdevDevice) udev_parent = NULL;
/* if the PCI ID is Intel then it's signed, no idea otherwise */
udev_parent = fu_udev_device_get_parent_with_subsystem(FU_UDEV_DEVICE(device), "pci");
if (udev_parent != NULL) {
if (!fu_device_probe(FU_DEVICE(udev_parent), error))
return FALSE;
if (fu_udev_device_get_vendor(udev_parent) == 0x8086)
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
}
/* success */
return TRUE;
}
static void
fu_thunderbolt_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, 100, "write");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload");
}
static void
fu_thunderbolt_device_init(FuThunderboltDevice *self)
{
FuThunderboltDevicePrivate *priv = GET_PRIVATE(self);
priv->auth_method = "nvm_authenticate";
fu_device_add_icon(FU_DEVICE(self), "thunderbolt");
fu_device_add_protocol(FU_DEVICE(self), "com.intel.thunderbolt");
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR);
}
static void
fu_thunderbolt_device_class_init(FuThunderboltDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
klass_device->activate = fu_thunderbolt_device_activate;
klass_device->to_string = fu_thunderbolt_device_to_string;
klass_device->probe = fu_thunderbolt_device_probe;
klass_device->prepare_firmware = fu_thunderbolt_device_prepare_firmware;
klass_device->write_firmware = fu_thunderbolt_device_write_firmware;
klass_device->attach = fu_thunderbolt_device_attach;
klass_device->rescan = fu_thunderbolt_device_rescan;
klass_device->set_progress = fu_thunderbolt_device_set_progress;
}