/* * Copyright (C) 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #define G_LOG_DOMAIN "FuMeiDevice" #include "config.h" #include #ifdef HAVE_MEI_H #include #endif #ifdef HAVE_IOCTL_H #include #endif #ifdef HAVE_ERRNO_H #include #endif #ifdef HAVE_SELECT_H #include #endif #include #include #include #include "fu-bytes.h" #include "fu-dump.h" #include "fu-mei-device.h" #include "fu-string.h" #define FU_MEI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ /** * FuMeiDevice * * The Intel proprietary Management Engine Interface. * * See also: #FuUdevDevice */ typedef struct { guint32 max_msg_length; guint8 protocol_version; gchar *uuid; gchar *parent_device_file; } FuMeiDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuMeiDevice, fu_mei_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_mei_device_get_instance_private(o)) static void fu_mei_device_to_string(FuDevice *device, guint idt, GString *str) { FuMeiDevice *self = FU_MEI_DEVICE(device); FuMeiDevicePrivate *priv = GET_PRIVATE(self); FU_DEVICE_CLASS(fu_mei_device_parent_class)->to_string(device, idt, str); fu_string_append(str, idt, "Uuid", priv->uuid); fu_string_append(str, idt, "ParentDeviceFile", priv->parent_device_file); if (priv->max_msg_length > 0x0) fu_string_append_kx(str, idt, "MaxMsgLength", priv->max_msg_length); if (priv->protocol_version > 0x0) fu_string_append_kx(str, idt, "ProtocolVer", priv->protocol_version); } static gboolean fu_mei_device_ensure_parent_device_file(FuMeiDevice *self, GError **error) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); const gchar *fn; g_autofree gchar *parent_tmp = NULL; g_autofree gchar *parent_mei_path = NULL; g_autoptr(FuUdevDevice) parent = NULL; g_autoptr(GDir) dir = NULL; /* get direct parent */ parent = fu_udev_device_get_parent_with_subsystem(FU_UDEV_DEVICE(self), NULL); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no MEI parent"); return FALSE; } /* look for the only child with this subsystem */ parent_mei_path = g_build_filename(fu_udev_device_get_sysfs_path(parent), "mei", NULL); dir = g_dir_open(parent_mei_path, 0, NULL); if (dir == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no MEI parent dir for %s", fu_udev_device_get_sysfs_path(parent)); return FALSE; } fn = g_dir_read_name(dir); if (fn == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no MEI parent in %s", parent_mei_path); return FALSE; } /* success */ parent_tmp = g_build_filename(fu_udev_device_get_sysfs_path(parent), "mei", fn, NULL); if (g_strcmp0(parent_tmp, priv->parent_device_file) != 0) { g_free(priv->parent_device_file); priv->parent_device_file = g_steal_pointer(&parent_tmp); } return TRUE; } static void fu_mei_device_set_uuid(FuMeiDevice *self, const gchar *uuid) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); if (g_strcmp0(priv->uuid, uuid) == 0) return; g_free(priv->uuid); priv->uuid = g_strdup(uuid); } static gboolean fu_mei_device_probe(FuDevice *device, GError **error) { FuMeiDevice *self = FU_MEI_DEVICE(device); FuMeiDevicePrivate *priv = GET_PRIVATE(self); const gchar *uuid; /* this has to exist */ uuid = fu_udev_device_get_sysfs_attr(FU_UDEV_DEVICE(device), "uuid", NULL); if (uuid == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "UUID not provided"); return FALSE; } fu_mei_device_set_uuid(self, uuid); fu_device_add_guid(device, uuid); /* get the mei[0-9] device file the parent is using */ if (!fu_mei_device_ensure_parent_device_file(self, error)) return FALSE; /* the kernel is missing `dev` on mei_me children */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)) == NULL) { g_autofree gchar *basename = g_path_get_basename(priv->parent_device_file); g_autofree gchar *device_file = g_build_filename("/dev", basename, NULL); fu_udev_device_set_device_file(FU_UDEV_DEVICE(device), device_file); } /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_mei_device_parent_class)->probe(device, error)) return FALSE; /* set the physical ID */ return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "pci", error); } static gchar * fu_mei_device_get_parent_attr(FuMeiDevice *self, const gchar *basename, guint idx, GError **error) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *fn = NULL; g_auto(GStrv) lines = NULL; g_autoptr(GBytes) blob = NULL; /* sanity check */ if (priv->parent_device_file == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent device file"); return NULL; } /* load lines */ fn = g_build_filename(priv->parent_device_file, basename, NULL); blob = fu_bytes_get_contents(fn, error); if (blob == NULL) return NULL; lines = fu_strsplit((const gchar *)g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), "\n", -1); if (g_strv_length(lines) <= idx) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "requested line %u of %u", idx, g_strv_length(lines)); return NULL; } /* success */ return g_strdup(lines[idx]); } /** * fu_mei_device_get_fw_ver: * @self: a #FuMeiDevice * @idx: line index * @error: (nullable): optional return location for an error * * Gets the firmware version for a specific index. * * Returns: string value * * Since: 1.8.7 **/ gchar * fu_mei_device_get_fw_ver(FuMeiDevice *self, guint idx, GError **error) { g_return_val_if_fail(FU_IS_MEI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_mei_device_get_parent_attr(self, "fw_ver", idx, error); } /** * fu_mei_device_get_fw_status: * @self: a #FuMeiDevice * @idx: line index * @error: (nullable): optional return location for an error * * Gets the firmware status for a specific index. * * Returns: string value * * Since: 1.8.7 **/ gchar * fu_mei_device_get_fw_status(FuMeiDevice *self, guint idx, GError **error) { g_return_val_if_fail(FU_IS_MEI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_mei_device_get_parent_attr(self, "fw_status", idx, error); } /** * fu_mei_device_get_max_msg_length: * @self: a #FuMeiDevice * * Gets the maximum message length. * * Returns: integer * * Since: 1.8.2 **/ guint32 fu_mei_device_get_max_msg_length(FuMeiDevice *self) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MEI_DEVICE(self), G_MAXUINT32); return priv->max_msg_length; } /** * fu_mei_device_get_protocol_version: * @self: a #FuMeiDevice * * Gets the protocol version, or 0x for unset. * * Returns: integer * * Since: 1.8.2 **/ guint8 fu_mei_device_get_protocol_version(FuMeiDevice *self) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MEI_DEVICE(self), G_MAXUINT8); return priv->protocol_version; } /** * fu_mei_device_connect: * @self: a #FuMeiDevice * @req_protocol_version: required protocol version, or 0 * @error: (nullable): optional return location for an error * * Connects to the MEI device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_mei_device_connect(FuMeiDevice *self, guchar req_protocol_version, GError **error) { #ifdef HAVE_MEI_H FuMeiDevicePrivate *priv = GET_PRIVATE(self); fwupd_guid_t guid_le = {0x0}; struct mei_client *cl; struct mei_connect_client_data data = {0x0}; g_return_val_if_fail(FU_IS_MEI_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fwupd_guid_from_string(priv->uuid, &guid_le, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return FALSE; if (g_getenv("FU_MEI_DEVICE_DEBUG") != NULL) fu_dump_raw(G_LOG_DOMAIN, "guid_le", (guint8 *)&guid_le, sizeof(guid_le)); memcpy(&data.in_client_uuid, &guid_le, sizeof(guid_le)); if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), IOCTL_MEI_CONNECT_CLIENT, (guint8 *)&data, NULL, /* rc */ FU_MEI_DEVICE_IOCTL_TIMEOUT, error)) return FALSE; cl = &data.out_client_properties; if (req_protocol_version > 0 && cl->protocol_version != req_protocol_version) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Intel MEI protocol version not supported %i", cl->protocol_version); return FALSE; } /* success */ priv->max_msg_length = cl->max_msg_length; priv->protocol_version = cl->protocol_version; return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "linux/mei.h not supported"); return FALSE; #endif } /** * fu_mei_device_read: * @self: a #FuMeiDevice * @buf: (out): data * @bufsz: size of @data * @bytes_read: (nullable): bytes read * @timeout_ms: timeout * @error: (nullable): optional return location for an error * * Read raw bytes from the device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_mei_device_read(FuMeiDevice *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, GError **error) { gssize rc; g_return_val_if_fail(FU_IS_MEI_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); rc = read(fu_udev_device_get_fd(FU_UDEV_DEVICE(self)), buf, bufsz); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "read failed %u: %s", (guint)rc, strerror(errno)); return FALSE; } if (g_getenv("FU_MEI_DEVICE_DEBUG") != NULL) fu_dump_raw(G_LOG_DOMAIN, "read", buf, rc); if (bytes_read != NULL) *bytes_read = (gsize)rc; return TRUE; } /** * fu_mei_device_write: * @self: a #FuMeiDevice * @buf: (out): data * @bufsz: size of @data * @timeout_ms: timeout * @error: (nullable): optional return location for an error * * Write raw bytes to the device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_mei_device_write(FuMeiDevice *self, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) { #ifdef HAVE_SELECT_H struct timeval tv; gssize written; gssize rc; fd_set set; guint fd = fu_udev_device_get_fd(FU_UDEV_DEVICE(self)); g_return_val_if_fail(FU_IS_MEI_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000000; if (g_getenv("FU_MEI_DEVICE_DEBUG") != NULL) fu_dump_raw(G_LOG_DOMAIN, "write", buf, bufsz); written = write(fd, buf, bufsz); if (written < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "write failed with status %" G_GSSIZE_FORMAT " %s", written, strerror(errno)); return FALSE; } if ((gsize)written != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "only wrote %" G_GSSIZE_FORMAT " of %" G_GSIZE_FORMAT, written, bufsz); return FALSE; } FD_ZERO(&set); FD_SET(fd, &set); rc = select(fd + 1, &set, NULL, NULL, &tv); if (rc > 0 && FD_ISSET(fd, &set)) return TRUE; /* timed out */ if (rc == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "write failed on timeout with status"); return FALSE; } /* rc < 0 */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "write failed on select with status %" G_GSSIZE_FORMAT, rc); return FALSE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "linux/select.h not supported"); return FALSE; #endif } static void fu_mei_device_incorporate(FuDevice *device, FuDevice *donor) { FuMeiDevice *self = FU_MEI_DEVICE(device); FuMeiDevicePrivate *priv = GET_PRIVATE(self); FuMeiDevicePrivate *priv_donor = GET_PRIVATE(FU_MEI_DEVICE(donor)); g_return_if_fail(FU_IS_MEI_DEVICE(self)); g_return_if_fail(FU_IS_MEI_DEVICE(donor)); /* FuUdevDevice->incorporate */ FU_DEVICE_CLASS(fu_mei_device_parent_class)->incorporate(device, donor); /* copy private instance data */ priv->max_msg_length = priv_donor->max_msg_length; priv->protocol_version = priv_donor->protocol_version; if (priv->uuid == NULL) fu_mei_device_set_uuid(self, priv_donor->uuid); if (priv->parent_device_file == NULL) priv->parent_device_file = g_strdup(priv_donor->parent_device_file); } static void fu_mei_device_init(FuMeiDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_udev_device_set_flags(FU_UDEV_DEVICE(self), FU_UDEV_DEVICE_FLAG_OPEN_READ | FU_UDEV_DEVICE_FLAG_OPEN_WRITE | FU_UDEV_DEVICE_FLAG_VENDOR_FROM_PARENT); } static void fu_mei_device_finalize(GObject *object) { FuMeiDevice *self = FU_MEI_DEVICE(object); FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->uuid); g_free(priv->parent_device_file); G_OBJECT_CLASS(fu_mei_device_parent_class)->finalize(object); } static void fu_mei_device_class_init(FuMeiDeviceClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_mei_device_finalize; klass_device->probe = fu_mei_device_probe; klass_device->to_string = fu_mei_device_to_string; klass_device->incorporate = fu_mei_device_incorporate; }