/* * Copyright (C) 2017 Christian J. Kellner * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fu-context-private.h" #include "fu-plugin-private.h" #include "fu-thunderbolt-firmware-update.h" #include "fu-thunderbolt-firmware.h" #include "fu-udev-device-private.h" static gchar * udev_mock_add_domain(UMockdevTestbed *bed, int id) { gchar *path; g_autofree gchar *name = NULL; name = g_strdup_printf("domain%d", id); path = umockdev_testbed_add_device(bed, "thunderbolt", name, NULL, "security", "secure", NULL, "DEVTYPE", "thunderbolt_domain", NULL); g_assert_nonnull(path); return path; } static gchar * udev_mock_add_nvmem(UMockdevTestbed *bed, gboolean active, const char *parent, int id) { g_autofree gchar *name = NULL; gchar *path; name = g_strdup_printf("%s%d", active ? "nvm_active" : "nvm_non_active", id); path = umockdev_testbed_add_device(bed, "nvmem", name, parent, "nvmem", "", NULL, NULL); g_assert_nonnull(path); return path; } typedef struct MockDevice MockDevice; struct MockDevice { const char *name; /* sysfs: device_name */ const char *id; /* sysfs: device */ const char *nvm_version; const char *nvm_parsed_version; int delay_ms; int domain_id; struct MockDevice *children; /* optionally filled out */ const char *uuid; }; typedef struct MockTree MockTree; struct MockTree { const MockDevice *device; MockTree *parent; GPtrArray *children; gchar *sysfs_parent; int sysfs_id; int sysfs_nvm_id; gchar *uuid; UMockdevTestbed *bed; gchar *path; gchar *nvm_non_active; gchar *nvm_active; guint nvm_authenticate; gchar *nvm_version; FuDevice *fu_device; }; static MockTree * mock_tree_new(MockTree *parent, MockDevice *device, int *id) { MockTree *node = g_slice_new0(MockTree); int current_id = (*id)++; node->device = device; node->sysfs_id = current_id; node->sysfs_nvm_id = current_id; node->parent = parent; if (device->uuid) node->uuid = g_strdup(device->uuid); else node->uuid = g_uuid_string_random(); node->nvm_version = g_strdup(device->nvm_version); return node; } static void mock_tree_free(MockTree *tree) { for (guint i = 0; i < tree->children->len; i++) { MockTree *child = g_ptr_array_index(tree->children, i); mock_tree_free(child); } g_ptr_array_free(tree->children, TRUE); if (tree->fu_device) g_object_unref(tree->fu_device); g_free(tree->uuid); if (tree->bed != NULL) { if (tree->nvm_active) { umockdev_testbed_uevent(tree->bed, tree->nvm_active, "remove"); umockdev_testbed_remove_device(tree->bed, tree->nvm_active); } if (tree->nvm_non_active) { umockdev_testbed_uevent(tree->bed, tree->nvm_non_active, "remove"); umockdev_testbed_remove_device(tree->bed, tree->nvm_non_active); } if (tree->path) { umockdev_testbed_uevent(tree->bed, tree->path, "remove"); umockdev_testbed_remove_device(tree->bed, tree->path); } g_object_unref(tree->bed); } g_free(tree->nvm_version); g_free(tree->nvm_active); g_free(tree->nvm_non_active); g_free(tree->path); g_free(tree->sysfs_parent); g_slice_free(MockTree, tree); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(MockTree, mock_tree_free); #pragma clang diagnostic pop static GPtrArray * mock_tree_init_children(MockTree *node, int *id) { GPtrArray *children = g_ptr_array_new(); MockDevice *iter; for (iter = node->device->children; iter && iter->name; iter++) { MockTree *child = mock_tree_new(node, iter, id); g_ptr_array_add(children, child); child->children = mock_tree_init_children(child, id); } return children; } static MockTree * mock_tree_init(MockDevice *device) { MockTree *tree; int devices = 0; tree = mock_tree_new(NULL, device, &devices); tree->children = mock_tree_init_children(tree, &devices); return tree; } static void mock_tree_dump(const MockTree *node, int level) { if (node->path) { g_debug("%*s * %s [%s] at %s", level, " ", node->device->name, node->uuid, node->path); g_debug("%*s non-active nvmem at %s", level, " ", node->nvm_non_active); g_debug("%*s active nvmem at %s", level, " ", node->nvm_active); } else { g_debug("%*s * %s [%s] %d", level, " ", node->device->name, node->uuid, node->sysfs_id); } for (guint i = 0; i < node->children->len; i++) { const MockTree *child = g_ptr_array_index(node->children, i); mock_tree_dump(child, level + 2); } } static void mock_tree_firmware_verify(const MockTree *node, GBytes *data) { g_autoptr(GFile) nvm_device = NULL; g_autoptr(GFile) nvm = NULL; g_autoptr(GInputStream) is = NULL; g_autoptr(GChecksum) chk = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *sum_data = NULL; const gchar *sum_disk = NULL; gsize s; sum_data = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, data); chk = g_checksum_new(G_CHECKSUM_SHA1); g_assert_nonnull(node); g_assert_nonnull(node->nvm_non_active); nvm_device = g_file_new_for_path(node->nvm_non_active); nvm = g_file_get_child(nvm_device, "nvmem"); is = (GInputStream *)g_file_read(nvm, NULL, &error); g_assert_no_error(error); g_assert_nonnull(is); do { g_autoptr(GBytes) b = NULL; const guchar *d; b = g_input_stream_read_bytes(is, 4096, NULL, &error); g_assert_no_error(error); g_assert_nonnull(is); d = g_bytes_get_data(b, &s); if (s > 0) g_checksum_update(chk, d, (gssize)s); } while (s > 0); sum_disk = g_checksum_get_string(chk); g_assert_cmpstr(sum_data, ==, sum_disk); } typedef gboolean (*MockTreePredicate)(const MockTree *node, gpointer data); static const MockTree * mock_tree_contains(const MockTree *node, MockTreePredicate predicate, gpointer data) { if (predicate(node, data)) return node; for (guint i = 0; i < node->children->len; i++) { const MockTree *child; const MockTree *match; child = g_ptr_array_index(node->children, i); match = mock_tree_contains(child, predicate, data); if (match != NULL) return match; } return NULL; } static gboolean mock_tree_all(const MockTree *node, MockTreePredicate predicate, gpointer data) { if (!predicate(node, data)) return FALSE; for (guint i = 0; i < node->children->len; i++) { const MockTree *child; child = g_ptr_array_index(node->children, i); if (!mock_tree_all(child, predicate, data)) return FALSE; } return TRUE; } static gboolean mock_tree_compare_uuid(const MockTree *node, gpointer data) { const gchar *uuid = (const gchar *)data; return g_str_equal(node->uuid, uuid); } static const MockTree * mock_tree_find_uuid(const MockTree *root, const char *uuid) { return mock_tree_contains(root, mock_tree_compare_uuid, (gpointer)uuid); } static gboolean mock_tree_node_have_fu_device(const MockTree *node, gpointer data) { return node->fu_device != NULL; } static void write_controller_fw(const gchar *nvm) { g_autoptr(GFile) nvm_device = NULL; g_autoptr(GFile) nvmem = NULL; g_autofree gchar *fw_path = NULL; g_autoptr(GFile) fw_file = NULL; g_autoptr(GInputStream) is = NULL; g_autoptr(GOutputStream) os = NULL; g_autoptr(GError) error = NULL; gssize n; fw_path = g_build_filename(TESTDATADIR, "thunderbolt/minimal-fw-controller.bin", NULL); fw_file = g_file_new_for_path(fw_path); g_assert_nonnull(fw_file); nvm_device = g_file_new_for_path(nvm); g_assert_nonnull(nvm_device); nvmem = g_file_get_child(nvm_device, "nvmem"); g_assert_nonnull(nvmem); os = (GOutputStream *)g_file_append_to(nvmem, G_FILE_CREATE_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(os); is = (GInputStream *)g_file_read(fw_file, NULL, &error); g_assert_no_error(error); g_assert_nonnull(is); n = g_output_stream_splice(os, is, G_OUTPUT_STREAM_SPLICE_NONE, NULL, &error); g_assert_no_error(error); g_assert_cmpuint(n, >, 0); } static gboolean mock_tree_attach_device(gpointer user_data) { MockTree *tree = (MockTree *)user_data; const MockDevice *dev = tree->device; g_autofree gchar *idstr = NULL; g_autofree gchar *authenticate = NULL; g_assert_nonnull(tree); g_assert_nonnull(tree->sysfs_parent); g_assert_nonnull(dev); idstr = g_strdup_printf("%d-%d", dev->domain_id, tree->sysfs_id); authenticate = g_strdup_printf("0x%x", tree->nvm_authenticate); tree->path = umockdev_testbed_add_device(tree->bed, "thunderbolt", idstr, tree->sysfs_parent, "device_name", dev->name, "device", dev->id, "vendor", "042", "vendor_name", "GNOME.org", "authorized", "1", "nvm_authenticate", authenticate, "nvm_version", tree->nvm_version, "unique_id", tree->uuid, NULL, "DEVTYPE", "thunderbolt_device", NULL); tree->nvm_non_active = udev_mock_add_nvmem(tree->bed, FALSE, tree->path, tree->sysfs_id); tree->nvm_active = udev_mock_add_nvmem(tree->bed, TRUE, tree->path, tree->sysfs_id); g_assert_nonnull(tree->path); g_assert_nonnull(tree->nvm_non_active); g_assert_nonnull(tree->nvm_active); write_controller_fw(tree->nvm_active); for (guint i = 0; i < tree->children->len; i++) { MockTree *child; child = g_ptr_array_index(tree->children, i); child->bed = g_object_ref(tree->bed); child->sysfs_parent = g_strdup(tree->path); g_timeout_add(child->device->delay_ms, mock_tree_attach_device, child); } return FALSE; } typedef struct SyncContext { MockTree *tree; GMainLoop *loop; } SyncContext; static gboolean on_sync_timeout(gpointer user_data) { SyncContext *ctx = (SyncContext *)user_data; g_main_loop_quit(ctx->loop); return FALSE; } static void sync_device_added(FuPlugin *plugin, FuDevice *device, gpointer user_data) { SyncContext *ctx = (SyncContext *)user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id(device); MockTree *target; target = (MockTree *)mock_tree_find_uuid(tree, uuid); if (target == NULL) { g_critical("Got device that could not be matched: %s", uuid); return; } if (target->fu_device != NULL) g_object_unref(target->fu_device); target->fu_device = g_object_ref(device); } static void sync_device_removed(FuPlugin *plugin, FuDevice *device, gpointer user_data) { SyncContext *ctx = (SyncContext *)user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id(device); MockTree *target; target = (MockTree *)mock_tree_find_uuid(tree, uuid); if (target == NULL) { g_warning("Got device that could not be matched: %s", uuid); return; } else if (target->fu_device == NULL) { g_warning("Got remove event for out-of-tree device %s", uuid); return; } g_object_unref(target->fu_device); target->fu_device = NULL; } static void mock_tree_sync(MockTree *root, FuPlugin *plugin, int timeout_ms) { g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); gulong id_add; gulong id_del; SyncContext ctx = { .tree = root, .loop = mainloop, }; id_add = g_signal_connect(plugin, "device-added", G_CALLBACK(sync_device_added), &ctx); id_del = g_signal_connect(plugin, "device-removed", G_CALLBACK(sync_device_removed), &ctx); if (timeout_ms > 0) g_timeout_add(timeout_ms, on_sync_timeout, &ctx); g_main_loop_run(mainloop); g_signal_handler_disconnect(plugin, id_add); g_signal_handler_disconnect(plugin, id_del); } typedef struct AttachContext { /* in */ MockTree *tree; GMainLoop *loop; /* out */ gboolean complete; } AttachContext; static void mock_tree_plugin_device_added(FuPlugin *plugin, FuDevice *device, gpointer user_data) { AttachContext *ctx = (AttachContext *)user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id(device); MockTree *target; target = (MockTree *)mock_tree_find_uuid(tree, uuid); if (target == NULL) { g_warning("Got device that could not be matched: %s", uuid); return; } g_set_object(&target->fu_device, device); if (mock_tree_all(tree, mock_tree_node_have_fu_device, NULL)) { ctx->complete = TRUE; g_main_loop_quit(ctx->loop); } } static gboolean mock_tree_settle(MockTree *root, FuPlugin *plugin) { g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); gulong id; AttachContext ctx = { .tree = root, .loop = mainloop, }; id = g_signal_connect(plugin, "device-added", G_CALLBACK(mock_tree_plugin_device_added), &ctx); g_main_loop_run(mainloop); g_signal_handler_disconnect(plugin, id); return ctx.complete; } static gboolean mock_tree_attach(MockTree *root, UMockdevTestbed *bed, FuPlugin *plugin) { root->bed = g_object_ref(bed); root->sysfs_parent = udev_mock_add_domain(bed, root->device->domain_id); g_assert_nonnull(root->sysfs_parent); g_timeout_add(root->device->delay_ms, mock_tree_attach_device, root); return mock_tree_settle(root, plugin); } /* the unused parameter makes the function signature compatible * with 'MockTreePredicate' */ static gboolean mock_tree_node_is_detached(const MockTree *node, gpointer unused) { gboolean ret = node->path == NULL; /* consistency checks: if ret, make sure we are * fully detached */ if (ret) { g_assert_null(node->nvm_active); g_assert_null(node->nvm_non_active); g_assert_null(node->bed); } else { g_assert_nonnull(node->nvm_active); g_assert_nonnull(node->nvm_non_active); g_assert_nonnull(node->bed); } return ret; } static void mock_tree_detach(MockTree *node) { UMockdevTestbed *bed; if (mock_tree_node_is_detached(node, NULL)) return; for (guint i = 0; i < node->children->len; i++) { MockTree *child = g_ptr_array_index(node->children, i); mock_tree_detach(child); g_free(child->sysfs_parent); child->sysfs_parent = NULL; } bed = node->bed; umockdev_testbed_uevent(bed, node->nvm_active, "remove"); umockdev_testbed_remove_device(bed, node->nvm_active); umockdev_testbed_uevent(bed, node->nvm_non_active, "remove"); umockdev_testbed_remove_device(bed, node->nvm_non_active); umockdev_testbed_uevent(bed, node->path, "remove"); umockdev_testbed_remove_device(bed, node->path); g_free(node->path); g_free(node->nvm_non_active); g_free(node->nvm_active); node->path = NULL; node->nvm_non_active = NULL; node->nvm_active = NULL; g_object_unref(bed); node->bed = NULL; } typedef enum UpdateResult { UPDATE_SUCCESS = 0, /* nvm_authenticate will report error condition */ UPDATE_FAIL_DEVICE_INTERNAL = 1, /* device to be updated will NOT re-appear */ UPDATE_FAIL_DEVICE_NOSHOW = 2 } UpdateResult; typedef struct UpdateContext { GFileMonitor *monitor; UpdateResult result; guint timeout; GBytes *data; UMockdevTestbed *bed; FuPlugin *plugin; MockTree *node; gchar *version; } UpdateContext; static void update_context_free(UpdateContext *ctx) { if (ctx == NULL) return; g_file_monitor_cancel(ctx->monitor); g_object_unref(ctx->bed); g_object_unref(ctx->plugin); g_object_unref(ctx->monitor); g_bytes_unref(ctx->data); g_free(ctx->version); g_free(ctx); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(UpdateContext, update_context_free); #pragma clang diagnostic pop static gboolean reattach_tree(gpointer user_data) { UpdateContext *ctx = (UpdateContext *)user_data; MockTree *node = ctx->node; g_debug("Mock update done, reattaching tree..."); node->bed = g_object_ref(ctx->bed); g_timeout_add(node->device->delay_ms, mock_tree_attach_device, node); return FALSE; } static void udev_file_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { UpdateContext *ctx = (UpdateContext *)user_data; gboolean ok; gsize len; g_autofree gchar *data = NULL; g_autoptr(GError) error = NULL; g_debug("Got update trigger"); ok = g_file_monitor_cancel(monitor); g_assert_true(ok); ok = g_file_load_contents(file, NULL, &data, &len, NULL, &error); g_assert_no_error(error); g_assert_true(ok); if (!g_str_has_prefix(data, "1")) return; /* verify the firmware is correct */ mock_tree_firmware_verify(ctx->node, ctx->data); g_debug("Removing tree below and including: %s", ctx->node->path); mock_tree_detach(ctx->node); ctx->node->nvm_authenticate = (guint)ctx->result; /* update the version only on "success" simulations */ if (ctx->result == UPDATE_SUCCESS) { g_free(ctx->node->nvm_version); ctx->node->nvm_version = g_strdup(ctx->version); } g_debug("Simulating update to '%s' with result: 0x%x", ctx->version, ctx->node->nvm_authenticate); if (ctx->result == UPDATE_FAIL_DEVICE_NOSHOW) { g_debug("Simulating no-show fail:" " device tree will not reappear"); return; } g_debug("Device tree reattachment in %3.2f seconds", ctx->timeout / 1000.0); g_timeout_add(ctx->timeout, reattach_tree, ctx); } static UpdateContext * mock_tree_prepare_for_update(MockTree *node, FuPlugin *plugin, const char *version, GBytes *fw_data, guint timeout_ms) { UpdateContext *ctx; g_autoptr(GFile) dir = NULL; g_autoptr(GFile) f = NULL; g_autoptr(GError) error = NULL; GFileMonitor *monitor; ctx = g_new0(UpdateContext, 1); dir = g_file_new_for_path(node->path); f = g_file_get_child(dir, "nvm_authenticate"); monitor = g_file_monitor_file(f, G_FILE_MONITOR_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(monitor); ctx->node = node; ctx->plugin = g_object_ref(plugin); ctx->bed = g_object_ref(node->bed); ctx->timeout = timeout_ms; ctx->monitor = monitor; ctx->version = g_strdup(version); ctx->data = g_bytes_ref(fw_data); g_signal_connect(monitor, "changed", G_CALLBACK(udev_file_changed_cb), ctx); return ctx; } static MockDevice root_one = { .name = "Laptop", .id = "0x23", .nvm_version = "20.2", .nvm_parsed_version = "20.02", .children = (MockDevice[]){ { .name = "Thunderbolt Cable", .id = "0x24", .nvm_version = "20.0", .nvm_parsed_version = "20.00", .children = (MockDevice[]){{ .name = "Thunderbolt Dock", .id = "0x25", .nvm_version = "10.0", .nvm_parsed_version = "10.00", }, { NULL, } }, }, { .name = "Thunderbolt Cable", .id = "0x24", .nvm_version = "23.0", .nvm_parsed_version = "23.00", .children = (MockDevice[]){{ .name = "Thunderbolt SSD", .id = "0x26", .nvm_version = "5.0", .nvm_parsed_version = "05.00", }, { NULL, }}, }, { NULL, }, }, }; typedef struct TestParam { gboolean initialize_tree; gboolean attach_and_coldplug; const char *firmware_file; } TestParam; typedef enum TestFlags { TEST_INITIALIZE_TREE = 1 << 0, TEST_ATTACH = 1 << 1, TEST_PREPARE_FIRMWARE = 1 << 2, TEST_PREPARE_ALL = TEST_INITIALIZE_TREE | TEST_ATTACH | TEST_PREPARE_FIRMWARE } TestFlags; #define TEST_INIT_FULL (GUINT_TO_POINTER(TEST_PREPARE_ALL)) #define TEST_INIT_NONE (GUINT_TO_POINTER(0)) typedef struct ThunderboltTest { UMockdevTestbed *bed; FuPlugin *plugin; GUdevClient *udev_client; /* if TestParam::initialize_tree */ MockTree *tree; /* if TestParam::firmware_file is nonnull */ GMappedFile *fw_file; GBytes *fw_data; } ThunderboltTest; static void fu_thunderbolt_gudev_uevent_cb(GUdevClient *gudev_client, const gchar *action, GUdevDevice *udev_device, ThunderboltTest *tt) { if (g_strcmp0(action, "add") == 0) { g_autoptr(FuUdevDevice) device = fu_udev_device_new(udev_device); g_autoptr(GError) error_local = NULL; if (!fu_plugin_runner_backend_device_added(tt->plugin, FU_DEVICE(device), &error_local)) g_debug("failed to add: %s", error_local->message); return; } if (g_strcmp0(action, "remove") == 0) { if (tt->tree->fu_device != NULL) fu_plugin_device_remove(tt->plugin, tt->tree->fu_device); return; } if (g_strcmp0(action, "change") == 0) { const gchar *uuid = g_udev_device_get_sysfs_attr(udev_device, "unique_id"); MockTree *target = (MockTree *)mock_tree_find_uuid(tt->tree, uuid); g_assert_nonnull(target); fu_udev_device_emit_changed(FU_UDEV_DEVICE(target->fu_device)); return; } } static void test_set_up(ThunderboltTest *tt, gconstpointer params) { TestFlags flags = GPOINTER_TO_UINT(params); gboolean ret; g_autofree gchar *pluginfn = NULL; g_autofree gchar *sysfs = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuContext) ctx = fu_context_new(); const gchar *udev_subsystems[] = {"thunderbolt", NULL}; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, &error); g_assert_no_error(error); g_assert_true(ret); tt->bed = umockdev_testbed_new(); g_assert_nonnull(tt->bed); sysfs = umockdev_testbed_get_sys_dir(tt->bed); g_debug("mock sysfs at %s", sysfs); tt->plugin = fu_plugin_new(ctx); g_assert_nonnull(tt->plugin); pluginfn = g_build_filename(PLUGINBUILDDIR, "libfu_plugin_thunderbolt." G_MODULE_SUFFIX, NULL); ret = fu_plugin_open(tt->plugin, pluginfn, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_startup(tt->plugin, &error); g_assert_no_error(error); g_assert_true(ret); if (flags & TEST_INITIALIZE_TREE) { tt->tree = mock_tree_init(&root_one); g_assert_nonnull(tt->tree); } if (!umockdev_in_mock_environment()) { g_warning("Need to run with umockdev-wrapper"); return; } tt->udev_client = g_udev_client_new(udev_subsystems); g_assert_nonnull(tt->udev_client); g_signal_connect(tt->udev_client, "uevent", G_CALLBACK(fu_thunderbolt_gudev_uevent_cb), tt); if (flags & TEST_ATTACH) { g_assert_true(flags & TEST_INITIALIZE_TREE); ret = mock_tree_attach(tt->tree, tt->bed, tt->plugin); g_assert_true(ret); } if (flags & TEST_PREPARE_FIRMWARE) { g_autofree gchar *fw_path = NULL; fw_path = g_build_filename(TESTDATADIR, "thunderbolt/minimal-fw.bin", NULL); tt->fw_file = g_mapped_file_new(fw_path, FALSE, &error); g_assert_no_error(error); g_assert_nonnull(tt->fw_file); tt->fw_data = g_mapped_file_get_bytes(tt->fw_file); g_assert_nonnull(tt->fw_data); } } static void test_tear_down(ThunderboltTest *tt, gconstpointer user_data) { g_object_unref(tt->plugin); g_object_unref(tt->bed); g_object_unref(tt->udev_client); if (tt->tree) mock_tree_free(tt->tree); if (tt->fw_data) g_bytes_unref(tt->fw_data); if (tt->fw_file) g_mapped_file_unref(tt->fw_file); } static gboolean test_tree_uuids(const MockTree *node, gpointer data) { const MockTree *root = (MockTree *)data; const gchar *uuid = node->uuid; const MockTree *found; g_assert_nonnull(uuid); g_debug("Looking for %s", uuid); found = mock_tree_find_uuid(root, uuid); g_assert_nonnull(node); g_assert_nonnull(found); g_assert_cmpstr(node->uuid, ==, found->uuid); /* return false so we traverse the whole tree */ return FALSE; } static void test_tree(ThunderboltTest *tt, gconstpointer user_data) { const MockTree *found; gboolean ret; g_autoptr(MockTree) tree = NULL; tree = mock_tree_init(&root_one); g_assert_nonnull(tree); mock_tree_dump(tree, 0); (void)mock_tree_contains(tree, test_tree_uuids, tree); found = mock_tree_find_uuid(tree, "nonexistentuuid"); g_assert_null(found); ret = mock_tree_attach(tree, tt->bed, tt->plugin); g_assert_true(ret); mock_tree_detach(tree); ret = mock_tree_all(tree, mock_tree_node_is_detached, NULL); g_assert_true(ret); } static gboolean _compare_images(FuThunderboltFirmware *firmware_old, FuThunderboltFirmware *firmware, GError **error) { if (fu_thunderbolt_firmware_is_host(firmware) != fu_thunderbolt_firmware_is_host(firmware_old)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect firmware mode, got %s, expected %s", fu_thunderbolt_firmware_is_host(firmware) ? "host" : "device", fu_thunderbolt_firmware_is_host(firmware_old) ? "host" : "device"); return FALSE; } if (fu_thunderbolt_firmware_get_vendor_id(firmware) != fu_thunderbolt_firmware_get_vendor_id(firmware_old)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device vendor, got 0x%04x, expected 0x%04x", fu_thunderbolt_firmware_get_vendor_id(firmware), fu_thunderbolt_firmware_get_vendor_id(firmware_old)); return FALSE; } if (fu_thunderbolt_firmware_get_device_id(firmware) != fu_thunderbolt_firmware_get_device_id(firmware_old)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device type, got 0x%04x, expected 0x%04x", fu_thunderbolt_firmware_get_device_id(firmware), fu_thunderbolt_firmware_get_device_id(firmware_old)); return FALSE; } if (fu_thunderbolt_firmware_get_model_id(firmware) != fu_thunderbolt_firmware_get_model_id(firmware_old)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device model, got 0x%04x, expected 0x%04x", fu_thunderbolt_firmware_get_model_id(firmware), fu_thunderbolt_firmware_get_model_id(firmware_old)); return FALSE; } if (fu_thunderbolt_firmware_get_has_pd(firmware_old) && !fu_thunderbolt_firmware_get_has_pd(firmware)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "new firmware is missing PD"); return FALSE; } if (fu_thunderbolt_firmware_get_flash_size(firmware) != fu_thunderbolt_firmware_get_flash_size(firmware_old)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect flash size"); return FALSE; } return TRUE; } static void test_image_validation(ThunderboltTest *tt, gconstpointer user_data) { gboolean ret; g_autofree gchar *ctl_path = NULL; g_autofree gchar *fwi_path = NULL; g_autofree gchar *bad_path = NULL; g_autoptr(GMappedFile) fwi_file = NULL; g_autoptr(GMappedFile) ctl_file = NULL; g_autoptr(GMappedFile) bad_file = NULL; g_autoptr(GBytes) fwi_data = NULL; g_autoptr(GBytes) ctl_data = NULL; g_autoptr(GBytes) bad_data = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuThunderboltFirmwareUpdate) firmware_fwi = fu_thunderbolt_firmware_update_new(); g_autoptr(FuThunderboltFirmware) firmware_ctl = fu_thunderbolt_firmware_new(); g_autoptr(FuThunderboltFirmware) firmware_bad = fu_thunderbolt_firmware_new(); /* image as if read from the controller (i.e. no headers) */ ctl_path = g_build_filename(TESTDATADIR, "thunderbolt/minimal-fw-controller.bin", NULL); ctl_file = g_mapped_file_new(ctl_path, FALSE, &error); g_assert_no_error(error); g_assert_nonnull(ctl_file); ctl_data = g_mapped_file_get_bytes(ctl_file); g_assert_nonnull(ctl_data); /* parse controller image */ ret = fu_firmware_parse(FU_FIRMWARE(firmware_ctl), ctl_data, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_true(ret); g_assert_no_error(error); /* valid firmware update image */ fwi_path = g_build_filename(TESTDATADIR, "thunderbolt/minimal-fw.bin", NULL); fwi_file = g_mapped_file_new(fwi_path, FALSE, &error); g_assert_no_error(error); g_assert_nonnull(fwi_file); fwi_data = g_mapped_file_get_bytes(fwi_file); g_assert_nonnull(fwi_data); /* parse */ ret = fu_firmware_parse(FU_FIRMWARE(firmware_fwi), fwi_data, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_true(ret); g_assert_no_error(error); /* a wrong/bad firmware update image */ bad_path = g_build_filename(TESTDATADIR, "colorhug/firmware.bin", NULL); bad_file = g_mapped_file_new(bad_path, FALSE, &error); g_assert_no_error(error); g_assert_nonnull(bad_file); bad_data = g_mapped_file_get_bytes(bad_file); g_assert_nonnull(bad_data); /* parse; should fail, bad image */ ret = fu_firmware_parse(FU_FIRMWARE(firmware_bad), bad_data, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_READ); g_debug("expected image validation error [ctl]: %s", error->message); g_clear_error(&error); /* now for some testing ... this should work */ ret = _compare_images(firmware_ctl, FU_THUNDERBOLT_FIRMWARE(firmware_fwi), &error); g_assert_no_error(error); g_assert_true(ret); } static void test_change_uevent(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; gboolean ret; const gchar *version_after; /* test sanity check */ g_assert_nonnull(tree); /* simulate change of version via a change even, i.e. * without add, remove. */ umockdev_testbed_set_attribute(tt->bed, tree->path, "nvm_version", "42.23"); umockdev_testbed_uevent(tt->bed, tree->path, "change"); /* we just "wait" for 500ms, should be enough */ mock_tree_sync(tree, plugin, 500); /* the tree should not have changed */ ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_true(ret); /* we should have the version change in the FuDevice */ version_after = fu_device_get_version(tree->fu_device); g_assert_cmpstr(version_after, ==, "42.23"); } static void test_update_working(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate an update, where the device goes away and comes back * after the time in the last parameter (given in ms) */ up_ctx = mock_tree_prepare_for_update(tree, plugin, "42.23", fw_data, 1000); ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error(error); g_assert_true(ret); /* we wait until the plugin has picked up all the * subtree changes */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); ret = fu_plugin_runner_attach(plugin, tree->fu_device, &error); g_assert_no_error(error); g_assert_true(ret); version_after = fu_device_get_version(tree->fu_device); g_debug("version after update: %s", version_after); g_assert_cmpstr(version_after, ==, "42.23"); /* make sure all pending events have happened */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); /* now we check if the every tree node has a corresponding FuDevice, * this implicitly checks that we are handling uevents correctly * after the event, and that we are in sync with the udev tree */ ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_true(ret); } static void test_update_wd19(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_before; const gchar *version_after; g_autoptr(GError) error = NULL; /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate a wd19 update which will not disappear / re-appear */ fu_device_add_flag(tree->fu_device, FWUPD_DEVICE_FLAG_SKIPS_RESTART); fu_device_add_flag(tree->fu_device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); version_before = fu_device_get_version(tree->fu_device); ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_has_flag(tree->fu_device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); g_assert_true(ret); version_after = fu_device_get_version(tree->fu_device); g_assert_cmpstr(version_after, ==, version_before); } static void test_update_fail(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate an update, as in test_update_working, * but simulate an error indicated by the device */ up_ctx = mock_tree_prepare_for_update(tree, plugin, "42.23", fw_data, 1000); up_ctx->result = UPDATE_FAIL_DEVICE_INTERNAL; ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error(error); g_assert_true(ret); /* we wait until the plugin has picked up all the * subtree changes, and make sure we still receive * udev updates correctly and are in sync */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); ret = fu_plugin_runner_attach(plugin, tree->fu_device, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_assert_false(ret); /* make sure all pending events have happened */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); /* version should *not* have changed (but we get parsed version) */ version_after = fu_device_get_version(tree->fu_device); g_debug("version after update: %s", version_after); g_assert_cmpstr(version_after, ==, tree->device->nvm_parsed_version); ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_true(ret); } static void test_update_fail_nowshow(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate an update, as in test_update_working, * but simulate an error indicated by the device */ up_ctx = mock_tree_prepare_for_update(tree, plugin, "42.23", fw_data, 1000); up_ctx->result = UPDATE_FAIL_DEVICE_NOSHOW; ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error(error); g_assert_true(ret); mock_tree_sync(tree, plugin, 500); ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_false(ret); } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); g_test_add("/thunderbolt/basic", ThunderboltTest, NULL, test_set_up, test_tree, test_tear_down); g_test_add("/thunderbolt/image-validation", ThunderboltTest, TEST_INIT_NONE, test_set_up, test_image_validation, test_tear_down); g_test_add("/thunderbolt/change-uevent", ThunderboltTest, GUINT_TO_POINTER(TEST_INITIALIZE_TREE | TEST_ATTACH), test_set_up, test_change_uevent, test_tear_down); g_test_add("/thunderbolt/update{working}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_working, test_tear_down); g_test_add("/thunderbolt/update{failing}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_fail, test_tear_down); g_test_add("/thunderbolt/update{failing-noshow}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_fail_nowshow, test_tear_down); g_test_add("/thunderbolt/update{delayed_activation}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_wd19, test_tear_down); return g_test_run(); }