| /* |
| * Copyright 2021 Richard Hughes <richard@hughsie.com> |
| * |
| * SPDX-License-Identifier: LGPL-2.1-or-later |
| */ |
| |
| #define G_LOG_DOMAIN "FuBackend" |
| |
| #include "config.h" |
| |
| #include <fwupdplugin.h> |
| |
| #include <glib-unix.h> |
| #include <glib/gstdio.h> |
| #include <linux/netlink.h> |
| #include <sys/socket.h> |
| |
| #include "fu-context-private.h" |
| #include "fu-engine-struct.h" |
| #include "fu-udev-backend.h" |
| #include "fu-udev-device-private.h" |
| |
| struct _FuUdevBackend { |
| FuBackend parent_instance; |
| gint netlink_fd; |
| GHashTable *map_paths; /* of str:None */ |
| GHashTable *coldplug_cache; /* of str:FuUdevBackendColdplugCacheItem */ |
| GPtrArray *dpaux_devices; /* of FuDpauxDevice */ |
| guint dpaux_devices_rescan_id; |
| gboolean done_coldplug; |
| }; |
| |
| typedef struct { |
| FuUdevDevice *udev_device; |
| GError *error; |
| } FuUdevBackendColdplugCacheItem; |
| |
| G_DEFINE_TYPE(FuUdevBackend, fu_udev_backend, FU_TYPE_BACKEND) |
| |
| #define FU_UDEV_BACKEND_DPAUX_RESCAN_DELAY 5 /* s */ |
| |
| static void |
| fu_udev_backend_coldplug_cache_item_free(FuUdevBackendColdplugCacheItem *item) |
| { |
| if (item->udev_device != NULL) |
| g_object_unref(item->udev_device); |
| if (item->error) |
| g_error_free(item->error); |
| g_free(item); |
| } |
| |
| static void |
| fu_udev_backend_coldplug_cache_add_device(FuUdevBackend *self, |
| const gchar *fn, |
| FuUdevDevice *udev_device) |
| { |
| if (!self->done_coldplug) { |
| FuUdevBackendColdplugCacheItem *item = g_new0(FuUdevBackendColdplugCacheItem, 1); |
| item->udev_device = g_object_ref(udev_device); |
| g_hash_table_insert(self->coldplug_cache, g_strdup(fn), item); |
| } |
| } |
| |
| static void |
| fu_udev_backend_coldplug_cache_add_error(FuUdevBackend *self, const gchar *fn, GError *error) |
| { |
| if (!self->done_coldplug) { |
| FuUdevBackendColdplugCacheItem *item = g_new0(FuUdevBackendColdplugCacheItem, 1); |
| item->error = g_error_copy(error); |
| g_hash_table_insert(self->coldplug_cache, g_strdup(fn), item); |
| } |
| } |
| |
| static void |
| fu_udev_backend_to_string(FuBackend *backend, guint idt, GString *str) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(backend); |
| fwupd_codec_string_append_bool(str, idt, "DoneColdplug", self->done_coldplug); |
| } |
| |
| static void |
| fu_udev_backend_rescan_dpaux_device(FuUdevBackend *self, FuDevice *dpaux_device) |
| { |
| FuDevice *device_tmp; |
| g_autoptr(FuDeviceLocker) locker = NULL; |
| g_autoptr(GError) error_local = NULL; |
| |
| /* find the device we enumerated */ |
| g_debug("looking for %s", fu_device_get_backend_id(dpaux_device)); |
| device_tmp = |
| fu_backend_lookup_by_id(FU_BACKEND(self), fu_device_get_backend_id(dpaux_device)); |
| |
| /* open */ |
| fu_device_probe_invalidate(dpaux_device); |
| locker = fu_device_locker_new(dpaux_device, &error_local); |
| if (locker == NULL) { |
| g_debug("failed to open device %s: %s", |
| fu_device_get_backend_id(dpaux_device), |
| error_local->message); |
| if (device_tmp != NULL) |
| fu_backend_device_removed(FU_BACKEND(self), FU_DEVICE(device_tmp)); |
| return; |
| } |
| if (device_tmp == NULL) { |
| fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(dpaux_device)); |
| return; |
| } |
| } |
| |
| static gboolean |
| fu_udev_backend_rescan_dpaux_devices_cb(gpointer user_data) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(user_data); |
| for (guint i = 0; i < self->dpaux_devices->len; i++) { |
| FuDevice *dpaux_device = g_ptr_array_index(self->dpaux_devices, i); |
| fu_udev_backend_rescan_dpaux_device(self, dpaux_device); |
| } |
| self->dpaux_devices_rescan_id = 0; |
| return FALSE; |
| } |
| |
| static void |
| fu_udev_backend_rescan_dpaux_devices(FuUdevBackend *self) |
| { |
| if (self->dpaux_devices_rescan_id != 0) |
| g_source_remove(self->dpaux_devices_rescan_id); |
| self->dpaux_devices_rescan_id = |
| g_timeout_add_seconds(FU_UDEV_BACKEND_DPAUX_RESCAN_DELAY, |
| fu_udev_backend_rescan_dpaux_devices_cb, |
| self); |
| } |
| |
| static FuUdevDevice * |
| fu_udev_backend_create_device(FuUdevBackend *self, const gchar *fn, GError **error); |
| |
| static void |
| fu_udev_backend_create_ddc_proxy(FuUdevBackend *self, FuUdevDevice *udev_device) |
| { |
| g_autofree gchar *proxy_sysfs_path = NULL; |
| g_autofree gchar *proxy_sysfs_real = NULL; |
| g_autoptr(FuUdevDevice) proxy = NULL; |
| g_autoptr(GError) error_local = NULL; |
| |
| proxy_sysfs_path = |
| g_build_filename(fu_udev_device_get_sysfs_path(udev_device), "ddc", NULL); |
| proxy_sysfs_real = fu_path_make_absolute(proxy_sysfs_path, &error_local); |
| if (proxy_sysfs_real == NULL) { |
| g_debug("failed to resolve %s: %s", proxy_sysfs_path, error_local->message); |
| return; |
| } |
| proxy = fu_udev_backend_create_device(self, proxy_sysfs_real, &error_local); |
| if (proxy == NULL) { |
| g_warning("failed to create DRM DDC device: %s", error_local->message); |
| return; |
| } |
| fu_device_add_private_flag(FU_DEVICE(proxy), FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS); |
| if (!fu_device_probe(FU_DEVICE(proxy), &error_local)) { |
| if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) |
| return; |
| g_warning("failed to probe DRM DDC device: %s", error_local->message); |
| return; |
| } |
| fu_device_add_private_flag(FU_DEVICE(udev_device), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); |
| fu_device_set_proxy(FU_DEVICE(udev_device), FU_DEVICE(proxy)); |
| } |
| |
| static GType |
| fu_udev_backend_get_gtype_for_subsystem_devtype(const gchar *subsystem, const gchar *devtype) |
| { |
| struct { |
| const gchar *subsystem; |
| const gchar *devtype; |
| GType gtype; |
| } map[] = { |
| {"mei", NULL, FU_TYPE_MEI_DEVICE}, |
| {"drm", NULL, FU_TYPE_DRM_DEVICE}, |
| {"usb", "usb_device", FU_TYPE_USB_DEVICE}, |
| {"i2c", NULL, FU_TYPE_I2C_DEVICE}, |
| {"i2c-dev", NULL, FU_TYPE_I2C_DEVICE}, |
| {"drm_dp_aux_dev", NULL, FU_TYPE_DPAUX_DEVICE}, |
| {"hidraw", NULL, FU_TYPE_HIDRAW_DEVICE}, |
| {"block", "disk", FU_TYPE_BLOCK_DEVICE}, |
| {"block", "partition", FU_TYPE_BLOCK_PARTITION}, |
| {"serio", NULL, FU_TYPE_SERIO_DEVICE}, |
| {"pci", NULL, FU_TYPE_PCI_DEVICE}, |
| {"video4linux", NULL, FU_TYPE_V4L_DEVICE}, |
| }; |
| for (guint i = 0; i < G_N_ELEMENTS(map); i++) { |
| if (g_strcmp0(subsystem, map[i].subsystem) == 0 && |
| (map[i].devtype == NULL || g_strcmp0(devtype, map[i].devtype) == 0)) { |
| return map[i].gtype; |
| } |
| } |
| return FU_TYPE_UDEV_DEVICE; |
| } |
| |
| static FuDevice * |
| fu_udev_backend_create_device_for_donor(FuBackend *backend, FuDevice *donor, GError **error) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(backend); |
| FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); |
| g_autoptr(FuDevice) device = NULL; |
| GType gtype = FU_TYPE_UDEV_DEVICE; |
| |
| /* ignore zram and loop block devices -- of which there are dozens on systems with snap */ |
| if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(donor)), "block") == 0) { |
| g_autofree gchar *basename = |
| g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(donor))); |
| if (g_str_has_prefix(basename, "zram") || g_str_has_prefix(basename, "loop")) { |
| g_set_error(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_NOT_SUPPORTED, |
| "ignoring uninteresting %s block device", |
| basename); |
| return NULL; |
| } |
| } |
| |
| /* create actual device with correct GType */ |
| gtype = fu_udev_backend_get_gtype_for_subsystem_devtype( |
| fu_udev_device_get_subsystem(FU_UDEV_DEVICE(donor)), |
| fu_udev_device_get_devtype(FU_UDEV_DEVICE(donor))); |
| if (gtype == FU_TYPE_UDEV_DEVICE) { |
| device = g_object_ref(donor); |
| } else { |
| device = g_object_new(gtype, "backend", FU_BACKEND(self), NULL); |
| fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_ALL); |
| if (!fu_device_probe(device, error)) { |
| g_prefix_error_literal(error, "failed to probe: "); |
| return NULL; |
| } |
| } |
| |
| /* these are used without a subclass */ |
| if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "msr") == 0) |
| fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ); |
| |
| /* the DRM device has a i2c device that is used for communicating with the scaler */ |
| if (gtype == FU_TYPE_DRM_DEVICE) |
| fu_udev_backend_create_ddc_proxy(self, FU_UDEV_DEVICE(device)); |
| |
| /* notify plugins using fu_plugin_add_udev_subsystem() */ |
| if (fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)) != NULL) { |
| g_autoptr(GPtrArray) possible_plugins = NULL; |
| g_autofree gchar *subsystem = |
| fu_udev_device_get_subsystem_devtype(FU_UDEV_DEVICE(device)); |
| |
| possible_plugins = |
| fu_context_get_plugin_names_for_udev_subsystem(ctx, subsystem, NULL); |
| if (possible_plugins != NULL) { |
| for (guint i = 0; i < possible_plugins->len; i++) { |
| const gchar *plugin_name = g_ptr_array_index(possible_plugins, i); |
| fu_device_add_possible_plugin(device, plugin_name); |
| } |
| } |
| } |
| |
| /* set in fu-self-test */ |
| if (g_getenv("FWUPD_SELF_TEST") != NULL) |
| fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_FAKE); |
| return g_steal_pointer(&device); |
| } |
| |
| static FuUdevDevice * |
| fu_udev_backend_create_device(FuUdevBackend *self, const gchar *fn, GError **error) |
| { |
| FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); |
| g_autoptr(FuDevice) device = NULL; |
| g_autoptr(FuUdevDevice) device_donor = NULL; |
| g_autoptr(GError) error_local = NULL; |
| |
| /* query the cache to avoid scanning parent devices multiple times */ |
| if (!self->done_coldplug) { |
| FuUdevBackendColdplugCacheItem *item = |
| g_hash_table_lookup(self->coldplug_cache, fn); |
| if (item != NULL) { |
| if (item->udev_device == NULL) { |
| if (error != NULL) |
| *error = g_error_copy(item->error); |
| return NULL; |
| } |
| return g_object_ref(item->udev_device); |
| } |
| } |
| |
| /* use a donor device to probe for the subsystem and devtype */ |
| device_donor = fu_udev_device_new(ctx, fn); |
| if (!fu_device_probe(FU_DEVICE(device_donor), &error_local)) { |
| fu_udev_backend_coldplug_cache_add_error(self, fn, error_local); |
| g_propagate_prefixed_error(error, |
| g_steal_pointer(&error_local), |
| "failed to probe donor: "); |
| return NULL; |
| } |
| device = fu_udev_backend_create_device_for_donor(FU_BACKEND(self), |
| FU_DEVICE(device_donor), |
| &error_local); |
| if (device == NULL) { |
| fu_udev_backend_coldplug_cache_add_error(self, fn, error_local); |
| g_propagate_error(error, g_steal_pointer(&error_local)); |
| return NULL; |
| } |
| fu_udev_backend_coldplug_cache_add_device(self, fn, FU_UDEV_DEVICE(device)); |
| return FU_UDEV_DEVICE(g_steal_pointer(&device)); |
| } |
| |
| static void |
| fu_udev_backend_device_add_from_device(FuUdevBackend *self, FuUdevDevice *device) |
| { |
| /* DP AUX devices are *weird* and can only read the DPCD when a DRM device is attached */ |
| if (g_strcmp0(fu_udev_device_get_subsystem(device), "drm_dp_aux_dev") == 0) { |
| /* add and rescan, regardless of if we can open it */ |
| g_ptr_array_add(self->dpaux_devices, g_object_ref(device)); |
| fu_udev_backend_rescan_dpaux_devices(self); |
| |
| /* open -- this might seem redundant, but it means the device is added at daemon |
| * coldplug rather than a few seconds later */ |
| if (!self->done_coldplug) { |
| g_autoptr(FuDeviceLocker) locker = NULL; |
| g_autoptr(GError) error_local = NULL; |
| |
| locker = fu_device_locker_new(FU_DEVICE(device), &error_local); |
| if (locker == NULL) { |
| g_debug("failed to open device %s: %s", |
| fu_device_get_backend_id(FU_DEVICE(device)), |
| error_local->message); |
| return; |
| } |
| fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device)); |
| } |
| return; |
| } |
| |
| /* success */ |
| fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device)); |
| } |
| |
| static void |
| fu_udev_backend_device_remove(FuUdevBackend *self, const gchar *sysfs_path) |
| { |
| FuDevice *device_tmp; |
| |
| /* find the device we enumerated */ |
| device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfs_path); |
| if (device_tmp != NULL) { |
| g_debug("UDEV %s removed", sysfs_path); |
| |
| /* rescan all the DP AUX devices if it or any DRM device disappears */ |
| if (g_ptr_array_remove(self->dpaux_devices, device_tmp) || |
| g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device_tmp)), "drm") == |
| 0) { |
| fu_udev_backend_rescan_dpaux_devices(self); |
| } |
| fu_backend_device_removed(FU_BACKEND(self), device_tmp); |
| } |
| } |
| |
| static gint |
| fu_udev_backend_device_number_sort_cb(gconstpointer a, gconstpointer b) |
| { |
| FuUdevDevice *device1 = *((FuUdevDevice **)a); |
| FuUdevDevice *device2 = *((FuUdevDevice **)b); |
| if (fu_udev_device_get_number(device1) < fu_udev_device_get_number(device2)) |
| return -1; |
| if (fu_udev_device_get_number(device1) > fu_udev_device_get_number(device2)) |
| return 1; |
| return 0; |
| } |
| |
| static void |
| fu_udev_backend_coldplug_subsystem(FuUdevBackend *self, const gchar *fn) |
| { |
| const gchar *basename; |
| g_autoptr(GDir) dir = NULL; |
| g_autoptr(GError) error_dir = NULL; |
| g_autoptr(GPtrArray) devices = |
| g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); |
| |
| dir = g_dir_open(fn, 0, &error_dir); |
| if (dir == NULL) { |
| if (!g_error_matches(error_dir, G_FILE_ERROR, G_FILE_ERROR_NOENT)) |
| g_debug("ignoring: %s", error_dir->message); |
| return; |
| } |
| while ((basename = g_dir_read_name(dir)) != NULL) { |
| g_autofree gchar *fn_full = g_build_filename(fn, basename, NULL); |
| g_autofree gchar *fn_real = NULL; |
| g_autoptr(GError) error_local = NULL; |
| g_autoptr(FuUdevDevice) device = NULL; |
| |
| if (!g_file_test(fn_full, G_FILE_TEST_IS_DIR)) |
| continue; |
| fn_real = fu_path_make_absolute(fn_full, &error_local); |
| if (fn_real == NULL) { |
| g_warning("failed to get symlink target for %s: %s", |
| fn_full, |
| error_local->message); |
| continue; |
| } |
| if (g_hash_table_contains(self->map_paths, fn_real)) { |
| g_debug("skipping duplicate %s", fn_real); |
| continue; |
| } |
| device = fu_udev_backend_create_device(self, fn_real, &error_local); |
| if (device == NULL) { |
| if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) |
| continue; |
| g_warning("failed to create device from %s: %s", |
| fn_real, |
| error_local->message); |
| continue; |
| } |
| g_hash_table_add(self->map_paths, g_steal_pointer(&fn_real)); |
| g_ptr_array_add(devices, g_steal_pointer(&device)); |
| } |
| |
| /* sort by device order (so video0 comes before video9) and add as a device */ |
| g_ptr_array_sort(devices, fu_udev_backend_device_number_sort_cb); |
| for (guint i = 0; i < devices->len; i++) { |
| FuUdevDevice *device = g_ptr_array_index(devices, i); |
| fu_udev_backend_device_add_from_device(self, device); |
| } |
| } |
| |
| /* if enabled, systemd takes the kernel event, runs the udev rules (which might rename devices) |
| * and then re-broadcasts on the udev netlink socket */ |
| static gboolean |
| fu_udev_backend_netlink_parse_blob(FuUdevBackend *self, GBytes *blob, GError **error) |
| { |
| FuUdevAction action = FU_UDEV_ACTION_UNKNOWN; |
| g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); |
| #ifdef HAVE_SYSTEMD |
| FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); |
| const guint8 *buf; |
| gsize bufsz = 0; |
| g_autoptr(FuStructUdevMonitorNetlinkHeader) st_hdr = NULL; |
| g_autoptr(FuUdevDevice) device_donor = NULL; |
| g_autoptr(GBytes) blob_payload = NULL; |
| |
| /* parse the buffer */ |
| st_hdr = fu_struct_udev_monitor_netlink_header_parse_bytes(blob, 0x0, error); |
| if (st_hdr == NULL) |
| return FALSE; |
| blob_payload = |
| fu_bytes_new_offset(blob, |
| fu_struct_udev_monitor_netlink_header_get_properties_off(st_hdr), |
| fu_struct_udev_monitor_netlink_header_get_properties_len(st_hdr), |
| error); |
| if (blob_payload == NULL) |
| return FALSE; |
| |
| /* split into lines */ |
| buf = g_bytes_get_data(blob_payload, &bufsz); |
| for (gsize i = 0; i < bufsz; i++) { |
| g_autofree gchar *kvstr = NULL; |
| g_auto(GStrv) kv = NULL; |
| |
| kvstr = fu_strsafe((const gchar *)buf + i, bufsz - i); |
| if (kvstr == NULL) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INTERNAL, |
| "invalid ASCII buffer"); |
| return FALSE; |
| } |
| kv = g_strsplit(kvstr, "=", 2); |
| if (g_strcmp0(kv[0], "ACTION") == 0) { |
| action = fu_udev_action_from_string(kv[1]); |
| if (action == FU_UDEV_ACTION_UNKNOWN) { |
| g_set_error(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INVALID_DATA, |
| "unknown action %s", |
| kv[1]); |
| return FALSE; |
| } |
| |
| /* we do not care about these */ |
| if (action == FU_UDEV_ACTION_BIND || action == FU_UDEV_ACTION_UNBIND) |
| return TRUE; |
| } else if (g_strcmp0(kv[0], "DEVPATH") == 0) { |
| g_autofree gchar *sysfspath = g_build_filename(sysfsdir, kv[1], NULL); |
| |
| /* something changed */ |
| if (action == FU_UDEV_ACTION_CHANGE) { |
| FuDevice *device_tmp = |
| fu_backend_lookup_by_id(FU_BACKEND(self), sysfspath); |
| if (device_tmp == NULL) |
| return TRUE; |
| if (g_strcmp0( |
| fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device_tmp)), |
| "drm") == 0) |
| fu_udev_backend_rescan_dpaux_devices(self); |
| g_set_object(&device_donor, FU_UDEV_DEVICE(device_tmp)); |
| } |
| |
| /* something got removed */ |
| if (action == FU_UDEV_ACTION_REMOVE) { |
| fu_udev_backend_device_remove(self, sysfspath); |
| return TRUE; |
| } |
| |
| /* something got added */ |
| if (action == FU_UDEV_ACTION_ADD) { |
| if (device_donor != NULL) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INVALID_DATA, |
| "already have a donor device"); |
| return FALSE; |
| } |
| device_donor = fu_udev_device_new(ctx, sysfspath); |
| } |
| } else if (g_strcmp0(kv[0], "SUBSYSTEM") == 0 && device_donor != NULL) { |
| fu_udev_device_set_subsystem(device_donor, kv[1]); |
| } else if (g_strcmp0(kv[0], "DEVTYPE") == 0 && device_donor != NULL) { |
| fu_udev_device_set_devtype(device_donor, kv[1]); |
| } else if (device_donor != NULL) { |
| fu_udev_device_add_property(device_donor, kv[0], kv[1]); |
| } |
| |
| /* next! */ |
| i += strlen(kvstr); |
| } |
| |
| /* notify the engine */ |
| if (action == FU_UDEV_ACTION_CHANGE) { |
| if (device_donor == NULL) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_NOT_FOUND, |
| "no device to change"); |
| return FALSE; |
| } |
| fu_backend_device_changed(FU_BACKEND(self), FU_DEVICE(device_donor)); |
| } |
| |
| /* now create the actual device from the donor */ |
| if (action == FU_UDEV_ACTION_ADD) { |
| g_autoptr(FuUdevDevice) device_actual = NULL; |
| if (device_donor == NULL) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INVALID_DATA, |
| "no new device to add"); |
| return FALSE; |
| } |
| device_actual = |
| FU_UDEV_DEVICE(fu_udev_backend_create_device_for_donor(FU_BACKEND(self), |
| FU_DEVICE(device_donor), |
| error)); |
| if (device_actual == NULL) |
| return FALSE; |
| |
| /* success */ |
| fu_udev_backend_device_add_from_device(self, device_actual); |
| } |
| #else |
| g_auto(GStrv) split = fu_strsplit_bytes(blob, "@", 2); |
| |
| action = fu_udev_action_from_string(split[0]); |
| if (action == FU_UDEV_ACTION_ADD) { |
| g_autofree gchar *sysfspath = g_build_filename(sysfsdir, split[1], NULL); |
| g_autoptr(FuUdevDevice) device = |
| fu_udev_backend_create_device(self, sysfspath, error); |
| if (device == NULL) |
| return FALSE; |
| fu_udev_backend_device_add_from_device(self, device); |
| } else if (action == FU_UDEV_ACTION_REMOVE) { |
| g_autofree gchar *sysfspath = g_build_filename(sysfsdir, split[1], NULL); |
| fu_udev_backend_device_remove(self, sysfspath); |
| } else if (action == FU_UDEV_ACTION_CHANGE) { |
| g_autofree gchar *sysfspath = g_build_filename(sysfsdir, split[1], NULL); |
| FuDevice *device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfspath); |
| if (device_tmp == NULL) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_NOT_FOUND, |
| "no device to change"); |
| return FALSE; |
| } |
| fu_backend_device_changed(FU_BACKEND(self), device_tmp); |
| } |
| #endif |
| |
| /* success */ |
| return TRUE; |
| } |
| |
| static gboolean |
| fu_udev_backend_netlink_cb(gint fd, GIOCondition condition, gpointer user_data) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(user_data); |
| gssize len; |
| guint8 buf[10240] = {0x0}; |
| g_autoptr(GBytes) blob = NULL; |
| g_autoptr(GError) error_local = NULL; |
| |
| len = recv(fd, buf, sizeof(buf), MSG_DONTWAIT); |
| if (len < 0) |
| return TRUE; |
| blob = g_bytes_new(buf, len); |
| if (!fu_udev_backend_netlink_parse_blob(self, blob, &error_local)) { |
| if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || |
| g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { |
| g_debug("ignoring netlink message: %s", error_local->message); |
| return TRUE; |
| } |
| g_warning("ignoring netlink message: %s", error_local->message); |
| return TRUE; |
| } |
| return TRUE; |
| } |
| |
| static gboolean |
| fu_udev_backend_netlink_setup(FuUdevBackend *self, GError **error) |
| { |
| struct sockaddr_nl nls = { |
| .nl_family = AF_NETLINK, |
| .nl_pid = getpid(), |
| #ifdef HAVE_SYSTEMD |
| .nl_groups = FU_UDEV_MONITOR_NETLINK_GROUP_UDEV, |
| #else |
| .nl_groups = FU_UDEV_MONITOR_NETLINK_GROUP_KERNEL, |
| #endif |
| }; |
| g_autoptr(GSource) source = NULL; |
| |
| /* minijail -p prevents access */ |
| if (nls.nl_pid <= 2) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INTERNAL, |
| "failed to get PID, perhaps sandboxed?"); |
| return FALSE; |
| } |
| |
| self->netlink_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT); |
| if (self->netlink_fd < 0) { |
| g_set_error(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INTERNAL, |
| "failed to connect to netlink: %s", |
| fwupd_strerror(errno)); |
| return FALSE; |
| } |
| if (bind(self->netlink_fd, (void *)&nls, sizeof(nls))) { |
| g_set_error(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INTERNAL, |
| "bind to udev socket failed: %s", |
| fwupd_strerror(errno)); |
| return FALSE; |
| } |
| source = g_unix_fd_source_new(self->netlink_fd, G_IO_IN); |
| g_source_set_static_name(source, "netlink"); |
| g_source_set_callback(source, (GSourceFunc)fu_udev_backend_netlink_cb, self, NULL); |
| g_source_set_can_recurse(source, TRUE); |
| g_source_attach(source, NULL); |
| |
| /* success */ |
| return TRUE; |
| } |
| |
| static gboolean |
| fu_udev_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) |
| { |
| FuContext *ctx = fu_backend_get_context(backend); |
| FuUdevBackend *self = FU_UDEV_BACKEND(backend); |
| g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); |
| g_autoptr(GPtrArray) udev_subsystems = fu_context_get_udev_subsystems(ctx); |
| |
| /* get all devices of class */ |
| fu_progress_set_id(progress, G_STRLOC); |
| fu_progress_set_steps(progress, udev_subsystems->len); |
| for (guint i = 0; i < udev_subsystems->len; i++) { |
| const gchar *subsystem = g_ptr_array_index(udev_subsystems, i); |
| g_autofree gchar *class_fn = NULL; |
| g_autofree gchar *bus_fn = NULL; |
| |
| /* we only care about subsystems, not subsystem:devtype matches */ |
| if (g_strstr_len(subsystem, -1, ":") != NULL) { |
| fu_progress_step_done(progress); |
| continue; |
| } |
| |
| class_fn = g_build_filename(sysfsdir, "class", subsystem, NULL); |
| if (g_file_test(class_fn, G_FILE_TEST_EXISTS)) { |
| fu_udev_backend_coldplug_subsystem(self, class_fn); |
| fu_progress_step_done(progress); |
| continue; |
| } |
| bus_fn = g_build_filename(sysfsdir, "bus", subsystem, "devices", NULL); |
| if (g_file_test(bus_fn, G_FILE_TEST_EXISTS)) { |
| fu_udev_backend_coldplug_subsystem(self, bus_fn); |
| fu_progress_step_done(progress); |
| continue; |
| } |
| fu_progress_step_done(progress); |
| } |
| |
| /* success */ |
| g_hash_table_remove_all(self->coldplug_cache); |
| self->done_coldplug = TRUE; |
| return TRUE; |
| } |
| |
| static gboolean |
| fu_udev_backend_setup(FuBackend *backend, |
| FuBackendSetupFlags flags, |
| FuProgress *progress, |
| GError **error) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(backend); |
| |
| /* set up hotplug events */ |
| if (flags & FU_BACKEND_SETUP_FLAG_USE_HOTPLUG) { |
| if (!fu_udev_backend_netlink_setup(self, error)) { |
| g_prefix_error_literal(error, "failed to set up netlink: "); |
| return FALSE; |
| } |
| } |
| |
| /* success */ |
| return TRUE; |
| } |
| |
| static FuDevice * |
| fu_udev_backend_get_device_parent(FuBackend *backend, |
| FuDevice *device, |
| const gchar *subsystem, |
| GError **error) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(backend); |
| g_autofree gchar *sysfs_path = NULL; |
| g_autoptr(FuUdevDevice) device_new = NULL; |
| |
| /* emulated */ |
| if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { |
| GType gtype = FU_TYPE_UDEV_DEVICE; |
| g_autofree gchar *physical_id = NULL; |
| if (subsystem != NULL) { |
| g_auto(GStrv) split = g_strsplit(subsystem, ":", 2); |
| gtype = fu_udev_backend_get_gtype_for_subsystem_devtype(split[0], split[1]); |
| physical_id = |
| g_strconcat(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), |
| "-parent:", |
| subsystem, |
| NULL); |
| } else { |
| physical_id = |
| g_strconcat(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), |
| "-parent", |
| NULL); |
| } |
| device_new = g_object_new(gtype, |
| "context", |
| fu_backend_get_context(backend), |
| "physical-id", |
| physical_id, |
| NULL); |
| fu_device_add_flag(FU_DEVICE(device_new), FWUPD_DEVICE_FLAG_EMULATED); |
| return FU_DEVICE(g_steal_pointer(&device_new)); |
| } |
| |
| sysfs_path = g_strdup(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device))); |
| if (sysfs_path == NULL) { |
| g_set_error_literal(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_INTERNAL, |
| "sysfs path undefined"); |
| return NULL; |
| } |
| |
| if (!g_file_test(sysfs_path, G_FILE_TEST_EXISTS)) { |
| g_set_error(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_NOT_SUPPORTED, |
| "sysfs path '%s' doesn't exist, perhaps a transient device.", |
| sysfs_path); |
| return NULL; |
| } |
| |
| /* lets just walk up the directories */ |
| while (1) { |
| g_autofree gchar *dirname = NULL; |
| g_autoptr(GError) error_local = NULL; |
| |
| /* done? */ |
| dirname = g_path_get_dirname(sysfs_path); |
| if (g_strcmp0(dirname, ".") == 0 || g_strcmp0(dirname, "/") == 0) |
| break; |
| |
| /* check has matching subsystem and devtype */ |
| device_new = fu_udev_backend_create_device(self, dirname, &error_local); |
| if (device_new != NULL) { |
| if (fu_udev_device_match_subsystem(device_new, subsystem)) { |
| if (subsystem != NULL) { |
| g_auto(GStrv) split = g_strsplit(subsystem, ":", 2); |
| fu_udev_device_set_subsystem(device_new, split[0]); |
| } |
| return FU_DEVICE(g_steal_pointer(&device_new)); |
| } |
| } else { |
| if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) |
| g_warning("failed to create device: %s", error_local->message); |
| } |
| |
| /* just swap, and go deeper */ |
| g_free(sysfs_path); |
| sysfs_path = g_steal_pointer(&dirname); |
| } |
| |
| /* failed */ |
| g_set_error(error, |
| FWUPD_ERROR, |
| FWUPD_ERROR_NOT_FOUND, |
| "no parent with subsystem %s", |
| subsystem); |
| return NULL; |
| } |
| |
| static FuDevice * |
| fu_udev_backend_create_device_impl(FuBackend *backend, const gchar *backend_id, GError **error) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(backend); |
| return FU_DEVICE(fu_udev_backend_create_device(self, backend_id, error)); |
| } |
| |
| static void |
| fu_udev_backend_finalize(GObject *object) |
| { |
| FuUdevBackend *self = FU_UDEV_BACKEND(object); |
| if (self->dpaux_devices_rescan_id != 0) |
| g_source_remove(self->dpaux_devices_rescan_id); |
| if (self->netlink_fd > 0) |
| g_close(self->netlink_fd, NULL); |
| g_hash_table_unref(self->map_paths); |
| g_hash_table_unref(self->coldplug_cache); |
| g_ptr_array_unref(self->dpaux_devices); |
| G_OBJECT_CLASS(fu_udev_backend_parent_class)->finalize(object); |
| } |
| |
| static void |
| fu_udev_backend_init(FuUdevBackend *self) |
| { |
| self->map_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); |
| self->coldplug_cache = |
| g_hash_table_new_full(g_str_hash, |
| g_str_equal, |
| g_free, |
| (GDestroyNotify)fu_udev_backend_coldplug_cache_item_free); |
| self->dpaux_devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); |
| } |
| |
| static void |
| fu_udev_backend_class_init(FuUdevBackendClass *klass) |
| { |
| GObjectClass *object_class = G_OBJECT_CLASS(klass); |
| FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); |
| object_class->finalize = fu_udev_backend_finalize; |
| backend_class->coldplug = fu_udev_backend_coldplug; |
| backend_class->setup = fu_udev_backend_setup; |
| backend_class->to_string = fu_udev_backend_to_string; |
| backend_class->get_device_parent = fu_udev_backend_get_device_parent; |
| backend_class->create_device = fu_udev_backend_create_device_impl; |
| backend_class->create_device_for_donor = fu_udev_backend_create_device_for_donor; |
| } |
| |
| FuBackend * |
| fu_udev_backend_new(FuContext *ctx) |
| { |
| return FU_BACKEND(g_object_new(FU_TYPE_UDEV_BACKEND, |
| "name", |
| "udev", |
| "context", |
| ctx, |
| "device-gtype", |
| FU_TYPE_UDEV_DEVICE, |
| NULL)); |
| } |