From 464425fb5c3396206851480c3eb9758f28c2c31d Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Fri, 16 Jul 2021 12:32:08 +1000 Subject: [PATCH] SMBIOS: try reading from /sys/class/dmi if direct access fails The raw SMBIOS tables that Linux exposes in /sys/firmware/dmi are restricted to being readable by root only. If running as non-root access is still permitted by fields that have been pre-parsed by the kernel in /sys/class/dmi, most of which are world-readable. This allows the daemon to load most HWIDs even if running as a non-root user, as is done on Chromium OS. --- data/tests/dmi/class/chassis_type | 1 + data/tests/dmi/class/sys_vendor | 1 + libfwupdplugin/fu-self-test.c | 28 +++++ libfwupdplugin/fu-smbios-private.h | 4 + libfwupdplugin/fu-smbios.c | 159 ++++++++++++++++++++++++++++- libfwupdplugin/fwupdplugin.map | 1 + 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 data/tests/dmi/class/chassis_type create mode 100644 data/tests/dmi/class/sys_vendor diff --git a/data/tests/dmi/class/chassis_type b/data/tests/dmi/class/chassis_type new file mode 100644 index 000000000..b6a7d89c6 --- /dev/null +++ b/data/tests/dmi/class/chassis_type @@ -0,0 +1 @@ +16 diff --git a/data/tests/dmi/class/sys_vendor b/data/tests/dmi/class/sys_vendor new file mode 100644 index 000000000..d1beb7188 --- /dev/null +++ b/data/tests/dmi/class/sys_vendor @@ -0,0 +1 @@ +FwupdTest diff --git a/libfwupdplugin/fu-self-test.c b/libfwupdplugin/fu-self-test.c index e3f06e446..98d3df1d6 100644 --- a/libfwupdplugin/fu-self-test.c +++ b/libfwupdplugin/fu-self-test.c @@ -376,6 +376,33 @@ fu_smbios_dt_func (void) g_assert_cmpstr (str, ==, "Hughski Limited"); } +static void +fu_smbios_class_func (void) +{ + g_autofree gchar *path = g_build_filename (TESTDATADIR_SRC, "dmi", "class", NULL); + g_autoptr(FuSmbios) smbios = fu_smbios_new (); + g_autoptr(GError) error = NULL; + gboolean ret; + const gchar *str; + guint8 byte; + + ret = fu_smbios_setup_from_kernel (smbios, path, &error); + g_assert_no_error (error); + g_assert (ret); + if (g_getenv ("VERBOSE") != NULL) { + g_autofree gchar *dump = fu_smbios_to_string (smbios); + g_debug ("%s", dump); + } + + str = fu_smbios_get_string (smbios, FU_SMBIOS_STRUCTURE_TYPE_SYSTEM, 4, &error); + g_assert_no_error (error); + g_assert_cmpstr (str, ==, "FwupdTest"); + + byte = fu_smbios_get_integer (smbios, FU_SMBIOS_STRUCTURE_TYPE_CHASSIS, 5, &error); + g_assert_no_error (error); + g_assert_cmpuint(byte, ==, 16); +} + static void fu_common_strsafe_func (void) { @@ -3220,6 +3247,7 @@ main (int argc, char **argv) g_test_add_func ("/fwupd/smbios", fu_smbios_func); g_test_add_func ("/fwupd/smbios3", fu_smbios3_func); g_test_add_func ("/fwupd/smbios{dt}", fu_smbios_dt_func); + g_test_add_func ("/fwupd/smbios{class}", fu_smbios_class_func); g_test_add_func ("/fwupd/firmware", fu_firmware_func); g_test_add_func ("/fwupd/firmware{dedupe}", fu_firmware_dedupe_func); g_test_add_func ("/fwupd/firmware{build}", fu_firmware_build_func); diff --git a/libfwupdplugin/fu-smbios-private.h b/libfwupdplugin/fu-smbios-private.h index 8e646ea5f..f2933f79c 100644 --- a/libfwupdplugin/fu-smbios-private.h +++ b/libfwupdplugin/fu-smbios-private.h @@ -21,3 +21,7 @@ gboolean fu_smbios_setup_from_file (FuSmbios *self, const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT; +gboolean fu_smbios_setup_from_kernel (FuSmbios *self, + const gchar *path, + GError **error) + G_GNUC_WARN_UNUSED_RESULT; diff --git a/libfwupdplugin/fu-smbios.c b/libfwupdplugin/fu-smbios.c index 72e105b19..1dbc0c727 100644 --- a/libfwupdplugin/fu-smbios.c +++ b/libfwupdplugin/fu-smbios.c @@ -341,6 +341,146 @@ fu_smbios_setup_from_file (FuSmbios *self, const gchar *filename, GError **error return fu_smbios_setup_from_data (self, (guint8 *) buf, sz, error); } + +static gboolean +fu_smbios_encode_string_from_kernel (FuSmbios *self, const gchar *file_contents, + guint8 smbios_type, guint8 smbios_offset, + GError **error) +{ + FuSmbiosItem *item = g_ptr_array_index (self->items, smbios_type); + + /* add value to string table */ + g_ptr_array_add (item->strings, g_strdup (file_contents)); + /* add string table index to SMBIOS table */ + fu_smbios_convert_dt_value (self, smbios_type, smbios_offset, item->strings->len); + return TRUE; +} + +static gboolean +fu_smbios_encode_byte_from_kernel (FuSmbios *self, const gchar *file_contents, + guint8 smbios_type, guint8 smbios_offset, + GError **error) +{ + gchar *endp; + gint64 value = g_ascii_strtoll (file_contents, &endp, 10); + + if (*endp != 0) { + g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, + "non-numeric values in numeric string: %s", endp); + return FALSE; + } + if (value < 0 || value > G_MAXUINT8) { + g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, + "value \"%s\" is not representable in a byte", + file_contents); + return FALSE; + } + + fu_smbios_convert_dt_value (self, smbios_type, smbios_offset, value); + return TRUE; +} + +/* + * The mapping from SMBIOS field to sysfs name can be found by mapping + * the field to a kernel property name in dmi_decode() + * (drivers/firmware/dmi_scan.c), then the property name to sysfs entry + * in dmi_id_init_attr_table() (drivers/firmware/dmi-id.c). This table + * lists each attribute exposed in /sys/class/dmi when CONFIG_DMIID is + * enabled, mapping to the SMBIOS field and a function that can convert + * the textual version of the field back into the raw SMBIOS table + * representation. + */ +#define SYSFS_DMI_FIELD(_name, _type, _offset, kind) \ + { .name = _name, .type = _type, .offset = _offset, .encode = fu_smbios_encode_ ## kind ## _from_kernel } +const struct kernel_dmi_field { + const gchar *name; + gboolean (*encode)(FuSmbios *, const gchar *, guint8, guint8, GError **); + guint8 type; + guint8 offset; +} KERNEL_DMI_FIELDS[] = { + SYSFS_DMI_FIELD("bios_vendor", 0, 4, string), + SYSFS_DMI_FIELD("bios_version", 0, 5, string), + SYSFS_DMI_FIELD("bios_date", 0, 8, string), + SYSFS_DMI_FIELD("sys_vendor", 1, 4, string), + SYSFS_DMI_FIELD("product_name", 1, 5, string), + SYSFS_DMI_FIELD("product_version", 1, 6, string), + SYSFS_DMI_FIELD("product_serial", 1, 7, string), + /* SYSFS_DMI_FIELD("product_uuid", 1, 8, uuid) */ + SYSFS_DMI_FIELD("product_family", 1, 26, string), + SYSFS_DMI_FIELD("product_sku", 1, 25, string), + SYSFS_DMI_FIELD("board_vendor", 2, 4, string), + SYSFS_DMI_FIELD("board_name", 2, 5, string), + SYSFS_DMI_FIELD("board_version", 2, 6, string), + SYSFS_DMI_FIELD("board_serial", 2, 7, string), + SYSFS_DMI_FIELD("board_asset_tag", 2, 8, string), + SYSFS_DMI_FIELD("chassis_vendor", 3, 4, string), + SYSFS_DMI_FIELD("chassis_type", 3, 5, byte), + SYSFS_DMI_FIELD("chassis_version", 3, 6, string), + SYSFS_DMI_FIELD("chassis_serial", 3, 7, string), + SYSFS_DMI_FIELD("chassis_asset_tag", 3, 8, string), +}; + +/** + * fu_smbios_setup_from_kernel: + * @self: a #FuSmbios + * @path: a directory path + * @error: (nullable): optional return location for an error + * + * Reads SMBIOS value from DMI values provided by the kernel, such as in + * /sys/class/dmi on Linux. + * + * Returns: %TRUE for success + * + * Since: 1.6.2 + **/ +gboolean +fu_smbios_setup_from_kernel (FuSmbios *self, const gchar *path, GError **error) +{ + gboolean any_success = FALSE; + + /* add fake structures */ + for (guint i = 0; i < FU_SMBIOS_STRUCTURE_TYPE_LAST; i++) { + FuSmbiosItem *item = g_new0 (FuSmbiosItem, 1); + item->type = i; + item->buf = g_byte_array_new (); + item->strings = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (self->items, item); + } + + /* parse every known field from the corresponding file */ + for (gsize i = 0; i < G_N_ELEMENTS (KERNEL_DMI_FIELDS); i++) { + const struct kernel_dmi_field *field = &KERNEL_DMI_FIELDS[i]; + gsize bufsz = 0; + g_autofree gchar *buf = NULL; + g_autofree gchar *fn = g_build_filename (path, field->name, NULL); + g_autoptr(GError) local_error = NULL; + + if (!g_file_get_contents (fn, &buf, &bufsz, &local_error)) { + g_debug ("unable to read SMBIOS data from %s: %s", fn, + local_error->message); + continue; + } + + /* trim trailing newline added by kernel */ + if (buf[bufsz - 1] == '\n') + buf[bufsz - 1] = 0; + + if (!field->encode (self, buf, field->type, field->offset, &local_error)) { + g_warning ("failed to parse SMBIOS data from %s: %s", fn, + local_error->message); + continue; + } + + any_success = TRUE; + } + if (!any_success) { + g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, + "failed to read any SMBIOS values from %s", path); + return FALSE; + } + return TRUE; +} + static gboolean fu_smbios_parse_ep32 (FuSmbios *self, const gchar *buf, gsize sz, GError **error) { @@ -559,6 +699,7 @@ fu_smbios_setup (FuSmbios *self, GError **error) g_autofree gchar *path = NULL; g_autofree gchar *path_dt = NULL; g_autofree gchar *sysfsfwdir = NULL; + const gchar *path_dmi_class = "/sys/class/dmi/id"; g_return_val_if_fail (FU_IS_SMBIOS (self), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); @@ -567,8 +708,22 @@ fu_smbios_setup (FuSmbios *self, GError **error) /* DMI */ path = g_build_filename (sysfsfwdir, "dmi", "tables", NULL); - if (g_file_test (path, G_FILE_TEST_EXISTS)) - return fu_smbios_setup_from_path (self, path, error); + if (g_file_test (path, G_FILE_TEST_EXISTS)) { + g_autoptr(GError) error_local = NULL; + if (!fu_smbios_setup_from_path (self, path, &error_local)) { + if (!g_error_matches (error_local, G_FILE_ERROR, G_FILE_ERROR_ACCES)) { + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + g_debug ("ignoring %s", error_local->message); + } + } + + /* the values the kernel parsed; these are world-readable */ + if (g_file_test (path_dmi_class, G_FILE_TEST_IS_DIR)) { + g_debug ("trying to read %s", path_dmi_class); + return fu_smbios_setup_from_kernel (self, path_dmi_class, error); + } /* DT */ path_dt = g_build_filename (sysfsfwdir, "devicetree", "base", NULL); diff --git a/libfwupdplugin/fwupdplugin.map b/libfwupdplugin/fwupdplugin.map index 508d25a41..f686eef72 100644 --- a/libfwupdplugin/fwupdplugin.map +++ b/libfwupdplugin/fwupdplugin.map @@ -866,6 +866,7 @@ LIBFWUPDPLUGIN_1.6.2 { fu_ifd_region_to_name; fu_ifd_region_to_string; fu_plugin_add_udev_subsystem; + fu_smbios_setup_from_kernel; fu_udev_device_get_children_with_subsystem; fu_udev_device_set_dev; local: *;