fwupd/plugins/uf2/fu-uf2-device.c
Richard Hughes 60d266cbc6 Add support for UF2 devices
A UF2 device exposes a VFAT block device (sometimes called a Mass
Storage Device) which has a virtual file called `INFO_UF2.TXT` where
metadata can be read. It may also have a the current firmware exported
as a file called `CURRENT.UF2` which is in a 512 byte-block UF2 format.

Writing any file to the MSD will cause the firmware to be written.
Sometimes the device will restart and the volume will be unmounted
and then mounted again. In some cases the volume may not “come back”
until the user manually puts the device back in programming mode.

Match the block devices using the VID*PID, UUID or label, and then
create a UF2 device which can be used to flash firmware.

Note: We only read metadata from allow-listed IDs to avoid causing
regressions on non-UF2 volumes. To get the UUID and label you can
use commands like:

    udisksctl info -b /dev/sda1
2021-12-21 10:06:18 +00:00

440 lines
12 KiB
C

/*
* Copyright (C) 2021 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include "fu-uf2-device.h"
#include "fu-uf2-firmware.h"
struct _FuUf2Device {
FuUdevDevice parent_instance;
guint64 family_id;
FuVolume *volume; /* non-null when fwupd has mounted it privately */
};
G_DEFINE_TYPE(FuUf2Device, fu_uf2_device, FU_TYPE_UDEV_DEVICE)
static FuFirmware *
fu_uf2_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
g_autoptr(FuFirmware) firmware = fu_uf2_firmware_new();
if (!fu_firmware_parse(firmware, fw, flags, error))
return NULL;
/* check the family_id matches if we can read the old firmware */
if (self->family_id > 0 && fu_firmware_get_idx(firmware) > 0 &&
self->family_id != fu_firmware_get_idx(firmware)) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"family ID was different, expected 0x%08x and got 0x%08x",
(guint)self->family_id,
(guint)fu_firmware_get_idx(firmware));
return NULL;
}
/* success: but return the raw data */
return fu_firmware_new_from_bytes(fw);
}
static gboolean
fu_uf2_device_probe_current_fw(FuDevice *device, GBytes *fw, GError **error)
{
g_autofree gchar *csum_sha256 = NULL;
g_autoptr(FuFirmware) firmware = fu_uf2_firmware_new();
g_autoptr(GBytes) fw_raw = NULL;
/* parse to get version */
if (!fu_firmware_parse(firmware, fw, FWUPD_INSTALL_FLAG_NONE, error))
return FALSE;
if (fu_firmware_get_version(firmware) != NULL)
fu_device_set_version(device, fu_firmware_get_version(firmware));
/* add instance ID for quirks */
if (fu_firmware_get_idx(firmware) != 0x0) {
g_autofree gchar *id0 = NULL;
id0 = g_strdup_printf("UF2\\FAMILY_%08X", (guint32)fu_firmware_get_idx(firmware));
fu_device_add_instance_id_full(device, id0, FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS);
}
/* add device checksum */
fw_raw = fu_firmware_get_bytes(firmware, error);
if (fw_raw == NULL)
return FALSE;
csum_sha256 = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, fw_raw);
fu_device_add_checksum(device, csum_sha256);
/* success */
return TRUE;
}
static gchar *
fu_block_device_get_full_path(FuUf2Device *self, const gchar *filename, GError **error)
{
const gchar *devfile = fu_udev_device_get_device_file(FU_UDEV_DEVICE(self));
g_autoptr(FuVolume) volume = NULL;
g_autofree gchar *mount_point = NULL;
/* sanity check */
if (devfile == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"invalid path: no devfile");
return NULL;
}
/* find volume */
volume = fu_common_get_volume_by_device(devfile, error);
if (volume == NULL)
return NULL;
/* success */
mount_point = fu_volume_get_mount_point(volume);
return g_build_filename(mount_point, filename, NULL);
}
static gboolean
fu_block_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FuProgress *progress,
FwupdInstallFlags flags,
GError **error)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
gssize wrote;
g_autofree gchar *fn = NULL;
g_autoptr(GBytes) fw = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(GOutputStream) ostr = NULL;
/* get blob */
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return FALSE;
/* open file for writing; no cleverness */
fn = fu_block_device_get_full_path(self, "FIRMWARE.UF2", error);
if (fn == NULL)
return FALSE;
file = g_file_new_for_path(fn);
ostr = G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error));
if (ostr == NULL)
return FALSE;
/* write in one chunk and let the kernel do the right thing :) */
wrote = g_output_stream_write(ostr,
g_bytes_get_data(fw, NULL),
g_bytes_get_size(fw),
NULL,
error);
if (wrote < 0)
return FALSE;
if ((gsize)wrote != g_bytes_get_size(fw)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"only wrote 0x%x bytes",
(guint)wrote);
return FALSE;
}
/* success */
return TRUE;
}
static GBytes *
fu_block_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
g_autofree gchar *fn = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(GInputStream) istr = NULL;
/* open for reading */
fn = fu_block_device_get_full_path(self, "CURRENT.UF2", error);
if (fn == NULL)
return NULL;
file = g_file_new_for_path(fn);
istr = G_INPUT_STREAM(g_file_read(file, NULL, error));
if (istr == NULL)
return NULL;
/* read all in one big chunk */
return fu_common_get_contents_stream(istr, G_MAXUINT32, error);
}
static FuFirmware *
fu_uf2_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
g_autoptr(FuFirmware) firmware = fu_uf2_firmware_new();
g_autoptr(GBytes) fw = NULL;
fw = fu_device_dump_firmware(device, progress, error);
if (fw == NULL)
return NULL;
if (!fu_firmware_parse(firmware, fw, FWUPD_INSTALL_FLAG_NONE, error))
return NULL;
return g_steal_pointer(&firmware);
}
static gboolean
fu_uf2_device_volume_mount(FuUf2Device *self, GError **error)
{
const gchar *devfile = fu_udev_device_get_device_file(FU_UDEV_DEVICE(self));
/* mount volume if required */
self->volume = fu_common_get_volume_by_device(devfile, error);
if (self->volume == NULL)
return FALSE;
return fu_volume_mount(self->volume, error);
}
static gboolean
fu_uf2_device_check_volume_mounted_cb(FuDevice *self, gpointer user_data, GError **error)
{
const gchar *devfile = fu_udev_device_get_device_file(FU_UDEV_DEVICE(user_data));
g_autoptr(FuVolume) volume = NULL;
/* mount volume if required */
volume = fu_common_get_volume_by_device(devfile, error);
if (volume == NULL)
return FALSE;
if (!fu_volume_is_mounted(volume)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"is not mounted");
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_uf2_device_open(FuDevice *device, GError **error)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
g_autoptr(GError) error_local = NULL;
/* wait for the user session to auto-mount the volume -- ideally we want to avoid using
* fu_volume_mount() which would make the volume only accessible by the fwupd user */
if (!fu_device_retry_full(device,
fu_uf2_device_check_volume_mounted_cb,
20, /* count */
50, /* ms */
device,
&error_local)) {
if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
/* maybe no session running? */
if (!fu_uf2_device_volume_mount(self, error))
return FALSE;
} else {
g_propagate_error(error, g_steal_pointer(&error_local));
return FALSE;
}
}
/* success */
return TRUE;
}
static gboolean
fu_uf2_device_close(FuDevice *device, GError **error)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
/* we only do this when mounting for the fwupd user */
if (self->volume != NULL) {
if (!fu_volume_unmount(self->volume, error))
return FALSE;
g_clear_object(&self->volume);
}
/* success */
return TRUE;
}
static gboolean
fu_uf2_device_setup(FuDevice *device, GError **error)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
gsize bufsz = 0;
g_autofree gchar *buf = NULL;
g_autofree gchar *fn1 = NULL;
g_autofree gchar *fn2 = NULL;
g_auto(GStrv) lines = NULL;
g_autoptr(GBytes) fw = NULL;
/* this has to exist */
fn1 = fu_block_device_get_full_path(self, "INFO_UF2.TXT", error);
if (fn1 == NULL)
return FALSE;
if (!g_file_get_contents(fn1, &buf, &bufsz, error))
return FALSE;
lines = fu_common_strnsplit(buf, bufsz, "\n", -1);
for (guint i = 0; lines[i] != NULL; i++) {
if (g_str_has_prefix(lines[i], "Model: ")) {
fu_device_set_name(device, lines[i] + 7);
} else if (g_str_has_prefix(lines[i], "Board-ID: ")) {
g_autofree gchar *devid = NULL;
devid = g_strdup_printf("UF2\\BOARD_%s", lines[i] + 10);
fu_device_add_instance_id(device, devid);
}
}
/* this might exist */
fn2 = fu_block_device_get_full_path(self, "CURRENT.UF2", error);
fw = fu_common_get_contents_bytes(fn2, NULL);
if (fw != NULL) {
if (!fu_uf2_device_probe_current_fw(device, fw, error))
return FALSE;
} else {
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER);
}
/* success */
return TRUE;
}
static gboolean
fu_uf2_device_probe(FuDevice *device, GError **error)
{
GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(device));
const gchar *tmp;
const gchar *uuid;
guint64 vid = 0;
guint64 pid = 0;
/* FuUdevDevice->probe */
if (!FU_DEVICE_CLASS(fu_uf2_device_parent_class)->probe(device, error))
return FALSE;
/* check is valid */
tmp = g_udev_device_get_property(udev_device, "ID_BUS");
if (g_strcmp0(tmp, "usb") != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"is not correct ID_BUS=%s, expected usb",
tmp);
return FALSE;
}
tmp = g_udev_device_get_property(udev_device, "ID_FS_TYPE");
if (g_strcmp0(tmp, "vfat") != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"is not correct ID_FS_TYPE=%s, expected vfat",
tmp);
return FALSE;
}
/* set the physical ID */
if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "block", error))
return FALSE;
/* more instance IDs */
tmp = g_udev_device_get_property(udev_device, "ID_VENDOR_ID");
if (tmp != NULL)
vid = g_ascii_strtoull(tmp, NULL, 16);
tmp = g_udev_device_get_property(udev_device, "ID_MODEL_ID");
if (tmp != NULL)
pid = g_ascii_strtoull(tmp, NULL, 16);
uuid = g_udev_device_get_property(udev_device, "ID_FS_UUID");
if (uuid != NULL && vid != 0x0 && pid != 0x0) {
g_autofree gchar *devid =
g_strdup_printf("USB\\VID_%04X&PID_%04X&UUID_%s", (guint)vid, (guint)pid, uuid);
fu_device_add_instance_id(device, devid);
}
if (vid != 0x0 && pid != 0x0) {
g_autofree gchar *devid =
g_strdup_printf("USB\\VID_%04X&PID_%04X", (guint)vid, (guint)pid);
fu_device_add_instance_id(device, devid);
}
if (vid != 0x0) {
g_autofree gchar *vendor_id = g_strdup_printf("USB:0x%04X", (guint)vid);
fu_device_add_vendor_id(device, vendor_id);
}
/* check the quirk matched to avoid mounting *all* vfat devices */
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"not marked as updatable in uf2.quirk");
return FALSE;
}
/* success */
return TRUE;
}
static void
fu_uf2_device_set_progress(FuDevice *self, FuProgress *progress)
{
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98); /* write */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2); /* reload */
}
static void
fu_uf2_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuUf2Device *self = FU_UF2_DEVICE(device);
FU_DEVICE_CLASS(fu_uf2_device_parent_class)->to_string(device, idt, str);
if (self->family_id > 0)
fu_common_string_append_kx(str, idt, "FamilyId", self->family_id);
}
static void
fu_uf2_device_init(FuUf2Device *self)
{
fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.uf2");
}
static void
fu_uf2_device_finalize(GObject *obj)
{
FuUf2Device *self = FU_UF2_DEVICE(obj);
/* should be done by ->close(), but check to be sure */
if (self->volume != NULL)
g_object_unref(self->volume);
G_OBJECT_CLASS(fu_uf2_device_parent_class)->finalize(obj);
}
static void
fu_uf2_device_class_init(FuUf2DeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
GObjectClass *klass_object = G_OBJECT_CLASS(klass);
klass_object->finalize = fu_uf2_device_finalize;
klass_device->to_string = fu_uf2_device_to_string;
klass_device->probe = fu_uf2_device_probe;
klass_device->setup = fu_uf2_device_setup;
klass_device->open = fu_uf2_device_open;
klass_device->close = fu_uf2_device_close;
klass_device->prepare_firmware = fu_uf2_device_prepare_firmware;
klass_device->set_progress = fu_uf2_device_set_progress;
klass_device->read_firmware = fu_uf2_device_read_firmware;
klass_device->write_firmware = fu_block_device_write_firmware;
klass_device->dump_firmware = fu_block_device_dump_firmware;
}