/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2007-2010 David Zeuthen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "udisksdaemon.h" #include "udisksdaemonutil.h" #include "udisksstate.h" #include "udiskslogging.h" #include "udiskslinuxdevice.h" #include "udiskslinuxprovider.h" #include "udiskslinuxblockobject.h" #include "udiskslinuxdriveobject.h" #if defined(HAVE_LIBSYSTEMD_LOGIN) #include #include #endif #if defined(HAVE_ELOGIND) && !defined(HAVE_LIBSYSTEMD_LOGIN) #include /* re-use HAVE_LIBSYSTEMD_LOGIN to not clutter the source file */ #define HAVE_LIBSYSTEMD_LOGIN 1 #endif #if defined(HAVE_LIBSYSTEMD_LOGIN) #define LOGIND_AVAILABLE() (access("/run/systemd/seats/", F_OK) >= 0) #endif /** * SECTION:udisksdaemonutil * @title: Utilities * @short_description: Various utility routines * * Various utility routines. */ /** * udisks_string_concat: * @a: First part * @b: Second part * * Returns: A new #GString holding the concatenation of the inputs. */ GString* udisks_string_concat (GString *a, GString *b) { GString *result; result = g_string_sized_new (a->len + b->len); g_string_append_len (result, a->str, a->len); g_string_append_len (result, b->str, b->len); return result; } gchar * udisks_daemon_util_subst_str (const gchar *str, const gchar *from, const gchar *to) { gchar **parts; gchar *result; parts = g_strsplit (str, from, 0); result = g_strjoinv (to, parts); g_strfreev (parts); return result; } /** * udisks_daemon_util_subst_str_and_escape: * @str: A command string with variables to replace. * @from: A variable name that should be expanded. * @to: The value for that variable. Will be escaped for shells before * substitution. * * Returns: The command string with the substituted variable. */ gchar * udisks_daemon_util_subst_str_and_escape (const gchar *str, const gchar *from, const gchar *to) { gchar *quoted_and_escaped; gchar *ret; quoted_and_escaped = g_shell_quote (to); ret = udisks_daemon_util_subst_str (str, from, quoted_and_escaped); g_free (quoted_and_escaped); return ret; } /** * udisks_string_wipe_and_free: * @string: A string with potentially unsafe content or %NULL. * * Wipes the buffer and frees the string. */ void udisks_string_wipe_and_free (GString *string) { if (string != NULL) { memset (string->str, '\0', string->len); g_string_free (string, TRUE); } } /** * udisks_variant_lookup_binary: * @dict: A dictionary #GVariant. * @name: The name of the item to lookup. * @out_text: (out): Return location for the binary text as #GString. * * Looks up binary data in a dictionary #GVariant and returns it as #GString. * * If the value is a bytestring ("ay"), it can contain arbitrary binary data * including '\0' values. If the value is a string ("s"), @out_text does not * include the terminating '\0' character. * * Returns: %TRUE if @dict contains an item @name of type "ay" or "s" that was * successfully stored in @out_text, and %FALSE otherwise. */ gboolean udisks_variant_lookup_binary (GVariant *dict, const gchar *name, GString **out_text) { GVariant* item = g_variant_lookup_value (dict, name, NULL); if (item) { gboolean ret = udisks_variant_get_binary (item, out_text); g_variant_unref (item); return ret; } return FALSE; } /** * udisks_variant_get_binary: * @value: A #GVariant of type "ay" or "s". * @out_text: (out): Return location for the binary text as #GString. * * Gets binary data contained in a BYTEARRAY or STRING #GVariant and returns * it as a #GString. * * If the value is a bytestring ("ay"), it can contain arbitrary binary data * including '\0' values. If the value is a string ("s"), @out_text does not * include the terminating '\0' character. * * Returns: %TRUE if @value is a bytestring or string #GVariant and was * successfully stored in @out_text, and %FALSE otherwise. */ gboolean udisks_variant_get_binary (GVariant *value, GString **out_text) { const gchar* str = NULL; gsize size = 0; if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) str = g_variant_get_string (value, &size); else if (g_variant_is_of_type (value, G_VARIANT_TYPE_BYTESTRING)) str = g_variant_get_fixed_array (value, &size, sizeof (guchar)); if (str) { *out_text = g_string_new_len (str, size); return TRUE; } return FALSE; } /** * udisks_decode_udev_string: * @str: An udev-encoded string or %NULL. * @fallback_str: String to use when @str can't be converted to a valid UTF-8 string. * * Unescapes sequences like \x20 to " " and ensures the returned string is valid UTF-8. * * If the string is not valid UTF-8, try as hard as possible to convert to UTF-8. * * If %NULL is passed, then %NULL is returned. * * See udev_util_encode_string() in libudev/libudev-util.c in the udev * tree for what kinds of strings can be used. * * Returns: A valid UTF-8 string that must be freed with g_free(). */ gchar * udisks_decode_udev_string (const gchar *str, const gchar *fallback_str) { GString *s; gchar *ret; const gchar *end_valid; guint n; if (str == NULL) { ret = NULL; goto out; } s = g_string_new (NULL); for (n = 0; str[n] != '\0'; n++) { if (str[n] == '\\') { gint val; if (str[n + 1] != 'x' || str[n + 2] == '\0' || str[n + 3] == '\0') { udisks_warning ("**** NOTE: malformed encoded string `%s'", str); break; } val = (g_ascii_xdigit_value (str[n + 2]) << 4) | g_ascii_xdigit_value (str[n + 3]); g_string_append_c (s, val); n += 3; } else { g_string_append_c (s, str[n]); } } if (!g_utf8_validate (s->str, -1, &end_valid)) { udisks_warning ("The string `%s' is not valid UTF-8. Invalid characters begins at `%s'", s->str, end_valid); if (fallback_str) { udisks_info ("Invalid string `%s' replaced by `%s'", s->str, fallback_str); ret = g_strdup (fallback_str); g_string_free (s, TRUE); } else { ret = g_strndup (s->str, end_valid - s->str); g_string_free (s, TRUE); } } else { ret = g_string_free (s, FALSE); } out: return ret; } /** * udisks_safe_append_to_object_path: * @str: A #GString to append to. * @s: A UTF-8 string. * * Appends @s to @str in a way such that only characters that can be * used in a D-Bus object path will be used. E.g. a character not in * [A-Z][a-z][0-9]_ will be escaped as _HEX where * HEX is a two-digit hexadecimal number. * * Note that his mapping is not bijective - e.g. you cannot go back * to the original string. */ void udisks_safe_append_to_object_path (GString *str, const gchar *s) { guint n; for (n = 0; s[n] != '\0'; n++) { gint c = s[n]; /* D-Bus spec sez: * * Each element must only contain the ASCII characters "[A-Z][a-z][0-9]_" */ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { g_string_append_c (str, c); } else { /* Escape bytes not in [A-Z][a-z][0-9] as _ */ g_string_append_printf (str, "_%02x", (guint) c); } } } /** * udisks_g_object_ref_foreach: * @object: A #GObject to ref. * @unused: Unused parameter. * * This is a helper function for g_list_foreach. It expects a function * that takes two parameters but the standard g_object_ref takes just one * and using it makes gcc sad. So this function just calls g_object_ref * and throws away the second parameter. */ void udisks_g_object_ref_foreach (gpointer object, gpointer unused) { g_return_if_fail (G_IS_OBJECT (object)); g_object_ref (G_OBJECT (object)); return; } /** * udisks_g_object_ref_copy: * @object: A #GObject to ref. * @unused: Unused parameter. * * This is a helper function for g_list_copy_deep. It expects copy function * that takes two parameters but the standard g_object_ref takes just one * and using it makes gcc sad. So this function just calls g_object_ref * and throws away the second parameter. */ void * udisks_g_object_ref_copy (gconstpointer object, gpointer unused) { g_return_val_if_fail (G_IS_OBJECT (object), NULL); return g_object_ref (G_OBJECT (object)); } /** * udisks_daemon_util_block_get_size: * @device: A #GUdevDevice for a top-level block device. * @out_media_available: (out): Return location for whether media is available or %NULL. * @out_media_change_detected: (out): Return location for whether media change is detected or %NULL. * * Gets the size of the @device top-level block device, checking for media in the process * * Returns: The size of @device or 0 if no media is available or if unknown. */ guint64 udisks_daemon_util_block_get_size (GUdevDevice *device, gboolean *out_media_available, gboolean *out_media_change_detected) { gboolean media_available = FALSE; gboolean media_change_detected = TRUE; guint64 size = 0; /* figuring out if media is available is a bit tricky */ if (g_udev_device_get_sysfs_attr_as_boolean (device, "removable")) { /* never try to open optical drives (might cause the door to close) or * floppy drives (makes noise) */ if (g_udev_device_get_property_as_boolean (device, "ID_DRIVE_FLOPPY")) { /* assume media available */ media_available = TRUE; media_change_detected = FALSE; } else if (g_udev_device_get_property_as_boolean (device, "ID_CDROM")) { /* Rely on (careful) work already done by udev's cdrom_id prober */ if (g_udev_device_get_property_as_boolean (device, "ID_CDROM_MEDIA")) media_available = TRUE; } else { gint fd; /* For the general case, just rely on open(2) failing with * ENOMEDIUM if no medium is inserted */ fd = open (g_udev_device_get_device_file (device), O_RDONLY); if (fd >= 0) { media_available = TRUE; close (fd); } } } else { /* not removable, so media is implicitly available */ media_available = TRUE; } if (media_available && size == 0 && media_change_detected) size = g_udev_device_get_sysfs_attr_as_uint64 (device, "size") * 512; if (out_media_available != NULL) *out_media_available = media_available; if (out_media_change_detected != NULL) *out_media_change_detected = media_change_detected; return size; } /** * udisks_daemon_util_resolve_link: * @path: A path * @name: Name of a symlink in @path. * * Resolves the symlink @path/@name. * * Returns: A canonicalized absolute pathname or %NULL if the symlink * could not be resolved. Free with g_free(). */ gchar * udisks_daemon_util_resolve_link (const gchar *path, const gchar *name) { gchar *full_path; gchar link_path[PATH_MAX]; gchar resolved_path[PATH_MAX]; gssize num; gboolean found_it; found_it = FALSE; full_path = g_build_filename (path, name, NULL); num = readlink (full_path, link_path, sizeof(link_path) - 1); if (num != -1) { char *absolute_path; gchar *full_path_dir; link_path[num] = '\0'; full_path_dir = g_path_get_dirname (full_path); absolute_path = g_build_filename (full_path_dir, link_path, NULL); g_free (full_path_dir); if (realpath (absolute_path, resolved_path) != NULL) { found_it = TRUE; } g_free (absolute_path); } g_free (full_path); if (found_it) return g_strdup (resolved_path); else return NULL; } /** * udisks_daemon_util_resolve_links: * @path: A path * @dir_name: Name of a directory in @path holding symlinks. * * Resolves all symlinks in @path/@dir_name. This can be used to * easily walk e.g. holders or slaves of block devices. * * Returns: An array of canonicalized absolute pathnames. Free with g_strfreev(). */ gchar ** udisks_daemon_util_resolve_links (const gchar *path, const gchar *dir_name) { gchar *s; GDir *dir; const gchar *name; GPtrArray *p; p = g_ptr_array_new (); s = g_build_filename (path, dir_name, NULL); dir = g_dir_open (s, 0, NULL); if (dir == NULL) goto out; while ((name = g_dir_read_name (dir)) != NULL) { gchar *resolved; resolved = udisks_daemon_util_resolve_link (s, name); if (resolved != NULL) g_ptr_array_add (p, resolved); } g_ptr_array_add (p, NULL); out: if (dir != NULL) g_dir_close (dir); g_free (s); return (gchar **) g_ptr_array_free (p, FALSE); } /** * udisks_daemon_util_setup_by_user: * @daemon: A #UDisksDaemon. * @object: The #GDBusObject that the call is on or %NULL. * @user: The user in question. * * Checks whether the device represented by @object (if any) has been * setup by @user. * * Returns: %TRUE if @object has been set-up by @user, %FALSE if not. */ gboolean udisks_daemon_util_setup_by_user (UDisksDaemon *daemon, UDisksObject *object, uid_t user) { gboolean ret; UDisksBlock *block = NULL; UDisksPartition *partition = NULL; UDisksState *state; uid_t setup_by_user; UDisksObject *crypto_object; ret = FALSE; state = udisks_daemon_get_state (daemon); block = udisks_object_get_block (object); if (block == NULL) goto out; partition = udisks_object_get_partition (object); /* loop devices */ if (udisks_state_has_loop (state, udisks_block_get_device (block), &setup_by_user)) { if (setup_by_user == user) { ret = TRUE; goto out; } } /* partition of a loop device */ if (partition != NULL) { UDisksObject *partition_object = NULL; partition_object = udisks_daemon_find_object (daemon, udisks_partition_get_table (partition)); if (partition_object != NULL) { if (udisks_daemon_util_setup_by_user (daemon, partition_object, user)) { ret = TRUE; g_object_unref (partition_object); goto out; } g_object_unref (partition_object); } } /* LUKS devices */ crypto_object = udisks_daemon_find_object (daemon, udisks_block_get_crypto_backing_device (block)); if (crypto_object != NULL) { UDisksBlock *crypto_block; crypto_block = udisks_object_peek_block (crypto_object); if (udisks_state_find_unlocked_crypto_dev (state, udisks_block_get_device_number (crypto_block), &setup_by_user)) { if (setup_by_user == user) { ret = TRUE; g_object_unref (crypto_object); goto out; } } g_object_unref (crypto_object); } /* MDRaid devices */ if (g_strcmp0 (udisks_block_get_mdraid (block), "/") != 0) { uid_t started_by_user; if (udisks_state_has_mdraid (state, udisks_block_get_device_number (block), &started_by_user)) { if (started_by_user == user) { ret = TRUE; goto out; } } } out: g_clear_object (&partition); g_clear_object (&block); return ret; } /* Need this until we can depend on a libpolkit with this bugfix * * http://cgit.freedesktop.org/polkit/commit/?h=wip/js-rule-files&id=224f7b892478302dccbe7e567b013d3c73d376fd */ static void _safe_polkit_details_insert (PolkitDetails *details, const gchar *key, const gchar *value) { if (value != NULL && strlen (value) > 0) polkit_details_insert (details, key, value); } static void _safe_polkit_details_insert_int (PolkitDetails *details, const gchar *key, gint value) { gchar buf[32]; snprintf (buf, sizeof buf, "%d", value); polkit_details_insert (details, key, buf); } static void _safe_polkit_details_insert_uint64 (PolkitDetails *details, const gchar *key, guint64 value) { gchar buf[32]; snprintf (buf, sizeof buf, "0x%08llx", (unsigned long long int) value); polkit_details_insert (details, key, buf); } static gboolean check_authorization_no_polkit (UDisksDaemon *daemon, UDisksObject *object, const gchar *action_id, GVariant *options, const gchar *message, GDBusMethodInvocation *invocation, GError **error) { gboolean ret = FALSE; uid_t caller_uid = -1; GError *sub_error = NULL; if (!udisks_daemon_util_get_caller_uid_sync (daemon, invocation, NULL, /* GCancellable* */ &caller_uid, &sub_error)) { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_FAILED, "Error getting uid for caller with bus name %s: %s (%s, %d)", g_dbus_method_invocation_get_sender (invocation), sub_error->message, g_quark_to_string (sub_error->domain), sub_error->code); g_clear_error (&sub_error); goto out; } /* only allow root */ if (caller_uid == 0) { ret = TRUE; } else { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_NOT_AUTHORIZED, "Not authorized to perform operation (polkit authority not available and caller is not uid 0)"); } out: return ret; } /** * udisks_daemon_util_check_authorization_sync: * @daemon: A #UDisksDaemon. * @object: (allow-none): The #GDBusObject that the call is on or %NULL. * @action_id: The action id to check for. * @options: (allow-none): A #GVariant to check for the auth.no_user_interaction option or %NULL. * @message: The message to convey (use N_). * @invocation: The invocation to check for. * * Checks if the caller represented by @invocation is authorized for * the action identified by @action_id, optionally displaying @message * if authentication is needed. Additionally, if the caller is not * authorized, the appropriate error is already returned to the caller * via @invocation. * * The calling thread is blocked for the duration of the authorization * check which could be a very long time since it may involve * presenting an authentication dialog and having a human user use * it. If auth.no_user_interaction in @options is %TRUE * no authentication dialog will be presented and the check is not * expected to take a long time. * * See for the variables that * can be used in @message but note that not all variables can be used * in all checks. For example, any check involving a #UDisksDrive or a * #UDisksBlock object can safely include the fragment * $(drive) since it will always expand to the name of * the drive, e.g. INTEL SSDSA2MH080G1GC (/dev/sda1) or * the block device file e.g. /dev/vg_lucifer/lv_root * or /dev/sda1. However this won't work for operations * that isn't on a drive or block device, for example calls on the * Manager * object. * * Returns: %TRUE if caller is authorized, %FALSE if not. */ gboolean udisks_daemon_util_check_authorization_sync (UDisksDaemon *daemon, UDisksObject *object, const gchar *action_id, GVariant *options, const gchar *message, GDBusMethodInvocation *invocation) { GError *error = NULL; if (!udisks_daemon_util_check_authorization_sync_with_error (daemon, object, action_id, options, message, invocation, &error)) { g_dbus_method_invocation_take_error (invocation, error); return FALSE; } return TRUE; } gboolean udisks_daemon_util_check_authorization_sync_with_error (UDisksDaemon *daemon, UDisksObject *object, const gchar *action_id, GVariant *options, const gchar *message, GDBusMethodInvocation *invocation, GError **error) { PolkitAuthority *authority = NULL; PolkitSubject *subject = NULL; PolkitDetails *details = NULL; PolkitCheckAuthorizationFlags flags = POLKIT_CHECK_AUTHORIZATION_FLAGS_NONE; PolkitAuthorizationResult *result = NULL; GError *sub_error = NULL; gboolean ret = FALSE; UDisksBlock *block = NULL; UDisksDrive *drive = NULL; UDisksPartition *partition = NULL; UDisksObject *block_object = NULL; UDisksObject *drive_object = NULL; gboolean auth_no_user_interaction = FALSE; const gchar *details_device = NULL; gchar *details_drive = NULL; authority = udisks_daemon_get_authority (daemon); if (authority == NULL) { ret = check_authorization_no_polkit (daemon, object, action_id, options, message, invocation, error); goto out; } subject = polkit_system_bus_name_new (g_dbus_method_invocation_get_sender (invocation)); if (options != NULL) { g_variant_lookup (options, "auth.no_user_interaction", "b", &auth_no_user_interaction); } if (!auth_no_user_interaction) flags = POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION; details = polkit_details_new (); polkit_details_insert (details, "polkit.message", message); polkit_details_insert (details, "polkit.gettext_domain", "udisks2"); /* Find drive associated with the block device, if any */ if (object != NULL) { block = udisks_object_get_block (object); if (block != NULL) { block_object = g_object_ref (object); drive_object = udisks_daemon_find_object (daemon, udisks_block_get_drive (block)); if (drive_object != NULL) drive = udisks_object_get_drive (drive_object); } partition = udisks_object_get_partition (object); if (drive == NULL) drive = udisks_object_get_drive (object); } if (block != NULL) details_device = udisks_block_get_preferred_device (block); /* If we have a drive, use vendor/model in the message (in addition to Block:preferred-device) */ if (drive != NULL) { gchar *s; const gchar *vendor; const gchar *model; vendor = udisks_drive_get_vendor (drive); model = udisks_drive_get_model (drive); if (vendor == NULL) vendor = ""; if (model == NULL) model = ""; if (strlen (vendor) > 0 && strlen (model) > 0) s = g_strdup_printf ("%s %s", vendor, model); else if (strlen (vendor) > 0) s = g_strdup (vendor); else s = g_strdup (model); if (block != NULL) { details_drive = g_strdup_printf ("%s (%s)", s, udisks_block_get_preferred_device (block)); } else { details_drive = s; s = NULL; } g_free (s); _safe_polkit_details_insert (details, "drive.wwn", udisks_drive_get_wwn (drive)); _safe_polkit_details_insert (details, "drive.serial", udisks_drive_get_serial (drive)); _safe_polkit_details_insert (details, "drive.vendor", udisks_drive_get_vendor (drive)); _safe_polkit_details_insert (details, "drive.model", udisks_drive_get_model (drive)); _safe_polkit_details_insert (details, "drive.revision", udisks_drive_get_revision (drive)); if (udisks_drive_get_removable (drive)) { const gchar *const *media_compat; GString *media_compat_str; const gchar *sep = ","; polkit_details_insert (details, "drive.removable", "true"); _safe_polkit_details_insert (details, "drive.removable.bus", udisks_drive_get_connection_bus (drive)); media_compat_str = g_string_new (NULL); media_compat = udisks_drive_get_media_compatibility (drive); if (media_compat) { guint i; for (i = 0; media_compat[i] && strlen(media_compat[i]); i++) { if (i) g_string_append (media_compat_str, sep); g_string_append (media_compat_str, media_compat[i]); } } _safe_polkit_details_insert (details, "drive.removable.media", media_compat_str->str); g_string_free (media_compat_str, TRUE); } } if (block != NULL) { _safe_polkit_details_insert (details, "id.type", udisks_block_get_id_type (block)); _safe_polkit_details_insert (details, "id.usage", udisks_block_get_id_usage (block)); _safe_polkit_details_insert (details, "id.version", udisks_block_get_id_version (block)); _safe_polkit_details_insert (details, "id.label", udisks_block_get_id_label (block)); _safe_polkit_details_insert (details, "id.uuid", udisks_block_get_id_uuid (block)); } if (partition != NULL) { _safe_polkit_details_insert_int (details, "partition.number", udisks_partition_get_number (partition)); _safe_polkit_details_insert (details, "partition.type", udisks_partition_get_type_ (partition)); _safe_polkit_details_insert_uint64 (details, "partition.flags", udisks_partition_get_flags (partition)); _safe_polkit_details_insert (details, "partition.name", udisks_partition_get_name (partition)); _safe_polkit_details_insert (details, "partition.uuid", udisks_partition_get_uuid (partition)); } /* Fall back to Block:preferred-device */ if (details_drive == NULL && block != NULL) details_drive = udisks_block_dup_preferred_device (block); if (details_device != NULL) polkit_details_insert (details, "device", details_device); if (details_drive != NULL) polkit_details_insert (details, "drive", details_drive); sub_error = NULL; result = polkit_authority_check_authorization_sync (authority, subject, action_id, details, flags, NULL, /* GCancellable* */ &sub_error); if (result == NULL) { if (sub_error->domain != POLKIT_ERROR) { /* assume polkit authority is not available (e.g. could be the service * manager returning org.freedesktop.systemd1.Masked) */ g_clear_error (&sub_error); ret = check_authorization_no_polkit (daemon, object, action_id, options, message, invocation, error); } else { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_FAILED, "Error checking authorization: %s (%s, %d)", sub_error->message, g_quark_to_string (sub_error->domain), sub_error->code); g_clear_error (&sub_error); } goto out; } if (!polkit_authorization_result_get_is_authorized (result)) { if (polkit_authorization_result_get_dismissed (result)) g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_NOT_AUTHORIZED_DISMISSED, "The authentication dialog was dismissed"); else g_set_error (error, UDISKS_ERROR, polkit_authorization_result_get_is_challenge (result) ? UDISKS_ERROR_NOT_AUTHORIZED_CAN_OBTAIN : UDISKS_ERROR_NOT_AUTHORIZED, "Not authorized to perform operation"); goto out; } ret = TRUE; out: g_free (details_drive); g_clear_object (&block_object); g_clear_object (&drive_object); g_clear_object (&block); g_clear_object (&partition); g_clear_object (&drive); g_clear_object (&subject); g_clear_object (&details); g_clear_object (&result); return ret; } /* ---------------------------------------------------------------------------------------------------- */ static gboolean dbus_freedesktop_guint32_get (GDBusMethodInvocation *invocation, GCancellable *cancellable, const gchar *method, guint32 *out_value, GError **error) { gboolean ret = FALSE; GError *local_error = NULL; GVariant *value; guint32 fetched = 0; const gchar *caller = g_dbus_method_invocation_get_sender (invocation); value = g_dbus_connection_call_sync (g_dbus_method_invocation_get_connection (invocation), "org.freedesktop.DBus", /* bus name */ "/org/freedesktop/DBus", /* object path */ "org.freedesktop.DBus", /* interface */ method, /* method */ g_variant_new ("(s)", caller), G_VARIANT_TYPE ("(u)"), G_DBUS_CALL_FLAGS_NONE, -1, /* timeout_msec */ cancellable, &local_error); if (value == NULL) { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_FAILED, "Error determining uid of caller %s: %s (%s, %d)", caller, local_error->message, g_quark_to_string (local_error->domain), local_error->code); g_clear_error (&local_error); goto out; } { G_STATIC_ASSERT (sizeof (uid_t) == sizeof (guint32)); G_STATIC_ASSERT (sizeof (pid_t) == sizeof (guint32)); } g_variant_get (value, "(u)", &fetched); if (out_value != NULL) *out_value = fetched; g_variant_unref (value); ret = TRUE; out: return ret; } /** * udisks_daemon_util_get_user_info: * @out_gid: (out) (allow-none): Return location for resolved gid or %NULL. * @out_user_name: (out) (allow-none): Return location for resolved user name or %NULL. * @error: Return location for error. * * Gets the UNIX group and user name for a user id. * * Returns: %TRUE if the user information was obtained, %FALSE otherwise */ gboolean udisks_daemon_util_get_user_info (const uid_t uid, gid_t *out_gid, gchar **out_user_name, GError **error) { struct passwd pwstruct; gchar pwbuf[8192]; struct passwd *pw = NULL; int rc; rc = getpwuid_r (uid, &pwstruct, pwbuf, sizeof pwbuf, &pw); if (rc == 0 && pw == NULL) { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_FAILED, "User with uid %d does not exist", (gint) uid); goto out; } else if (pw == NULL) { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_FAILED, "Error looking up passwd struct for uid %d: %m", (gint) uid); goto out; } if (out_gid != NULL) *out_gid = pw->pw_gid; if (out_user_name != NULL) *out_user_name = g_strdup (pwstruct.pw_name); return TRUE; out: return FALSE; } /** * udisks_daemon_util_get_caller_uid_sync: * @daemon: A #UDisksDaemon. * @invocation: A #GDBusMethodInvocation. * @cancellable: (allow-none): A #GCancellable or %NULL. * @out_uid: (out): Return location for resolved uid or %NULL. * @error: Return location for error. * * Gets the UNIX user id of the peer represented by @invocation. * * Returns: %TRUE if the user id (and possibly group id) was obtained, %FALSE otherwise */ gboolean udisks_daemon_util_get_caller_uid_sync (UDisksDaemon *daemon, GDBusMethodInvocation *invocation, GCancellable *cancellable, uid_t *out_uid, GError **error) { gboolean ret; uid_t uid; /* TODO: cache this on @daemon */ ret = FALSE; if (!dbus_freedesktop_guint32_get (invocation, cancellable, "GetConnectionUnixUser", &uid, error)) { goto out; } if (out_uid != NULL) *out_uid = uid; ret = TRUE; out: return ret; } /* ---------------------------------------------------------------------------------------------------- */ /** * udisks_daemon_util_get_caller_pid_sync: * @daemon: A #UDisksDaemon. * @invocation: A #GDBusMethodInvocation. * @cancellable: (allow-none): A #GCancellable or %NULL. * @out_pid: (out): Return location for resolved pid or %NULL. * @error: Return location for error. * * Gets the UNIX process id of the peer represented by @invocation. * * Returns: %TRUE if the process id was obtained, %FALSE otherwise */ gboolean udisks_daemon_util_get_caller_pid_sync (UDisksDaemon *daemon, GDBusMethodInvocation *invocation, GCancellable *cancellable, pid_t *out_pid, GError **error) { /* "GetConnectionUnixProcessID" */ /* TODO: cache this on @daemon */ /* NOTE: pid_t is a signed 32 bit, but the * GetConnectionUnixProcessID dbus method returns an unsigned */ return dbus_freedesktop_guint32_get (invocation, cancellable, "GetConnectionUnixProcessID", (guint32*)(out_pid), error); } /* ---------------------------------------------------------------------------------------------------- */ /** * udisks_daemon_util_dup_object: * @interface_: (type GDBusInterface): A #GDBusInterface-derived instance. * @error: %NULL, or an unset #GError to set if the return value is %NULL. * * Gets the enclosing #UDisksObject for @interface, if any. * * Returns: (transfer full) (type UDisksObject): Either %NULL or a * #UDisksObject-derived instance that must be released with * g_object_unref(). */ gpointer udisks_daemon_util_dup_object (gpointer interface_, GError **error) { gpointer ret; g_return_val_if_fail (G_IS_DBUS_INTERFACE (interface_), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); ret = g_dbus_interface_dup_object (interface_); if (ret == NULL) { g_set_error (error, UDISKS_ERROR, UDISKS_ERROR_FAILED, "No enclosing object for interface"); } return ret; } /** * udisks_daemon_util_on_user_seat: * @daemon: A #UDisksDaemon. * @object: The #GDBusObject that the call is on or %NULL. * @user: The user to check for. * * Checks whether the device represented by @object (if any) is plugged into * a seat where the caller represented by @user is logged in and active. * * This works if @object is a drive or a block object. * * Returns: %TRUE if @object is on the same seat as one of @user's * active sessions, %FALSE otherwise. */ gboolean udisks_daemon_util_on_user_seat (UDisksDaemon *daemon, UDisksObject *object, uid_t user) { #if !defined(HAVE_LIBSYSTEMD_LOGIN) /* if we don't have systemd, assume it's always the same seat */ return TRUE; #else gboolean ret = FALSE; char *session = NULL; char *seat = NULL; const gchar *drive_seat; UDisksObject *drive_object = NULL; UDisksDrive *drive = NULL; /* if we don't have logind, assume it's always the same seat */ if (!LOGIND_AVAILABLE()) return TRUE; if (UDISKS_IS_LINUX_BLOCK_OBJECT (object)) { UDisksLinuxBlockObject *linux_block_object; UDisksBlock *block; linux_block_object = UDISKS_LINUX_BLOCK_OBJECT (object); block = udisks_object_get_block (UDISKS_OBJECT (linux_block_object)); if (block != NULL) { drive_object = udisks_daemon_find_object (daemon, udisks_block_get_drive (block)); g_object_unref (block); } } else if (UDISKS_IS_LINUX_DRIVE_OBJECT (object)) { drive_object = g_object_ref (object); } if (drive_object == NULL) goto out; drive = udisks_object_get_drive (UDISKS_OBJECT (drive_object)); if (drive == NULL) goto out; drive_seat = udisks_drive_get_seat (drive); if (drive_seat != NULL && sd_uid_is_on_seat (user, TRUE, drive_seat) > 0) { ret = TRUE; goto out; } out: free (seat); free (session); g_clear_object (&drive_object); g_clear_object (&drive); return ret; #endif /* HAVE_LIBSYSTEMD_LOGIN */ } /** * udisks_daemon_util_hexdump: * @data: Pointer to data. * @len: Length of data. * * Utility function to generate a hexadecimal representation of @len * bytes of @data. * * Returns: A multi-line string. Free with g_free() when done using it. */ gchar * udisks_daemon_util_hexdump (gconstpointer data, gsize len) { const guchar *bdata = data; guint n, m; GString *ret; ret = g_string_new (NULL); for (n = 0; n < len; n += 16) { g_string_append_printf (ret, "%04x: ", n); for (m = n; m < n + 16; m++) { if (m > n && (m%4) == 0) g_string_append_c (ret, ' '); if (m < len) g_string_append_printf (ret, "%02x ", (guint) bdata[m]); else g_string_append (ret, " "); } g_string_append (ret, " "); for (m = n; m < len && m < n + 16; m++) g_string_append_c (ret, g_ascii_isprint (bdata[m]) ? bdata[m] : '.'); g_string_append_c (ret, '\n'); } return g_string_free (ret, FALSE); } /** * udisks_daemon_util_hexdump_debug: * @data: Pointer to data. * @len: Length of data. * * Utility function to dumps the hexadecimal representation of @len * bytes of @data generated with udisks_daemon_util_hexdump() using * udisks_debug(). */ void udisks_daemon_util_hexdump_debug (gconstpointer data, gsize len) { gchar *s = udisks_daemon_util_hexdump (data, len); udisks_debug ("Hexdump of %" G_GSIZE_FORMAT " bytes:\n%s", len, s); g_free (s); } /* ---------------------------------------------------------------------------------------------------- */ /** * udisks_daemon_util_file_set_contents: * @filename: (type filename): Name of a file to write @contents to, in the GLib file name encoding. * @contents: (array length=length) (element-type guint8): String to write to the file. * @contents_len: Length of @contents, or -1 if @contents is a NUL-terminated string. * @mode_for_new_file: Mode for new file. * @error: Return location for a #GError, or %NULL. * * Like g_file_set_contents() but preserves the mode of the file if it * already exists and sets it to @mode_for_new_file otherwise. * * Return value: %TRUE on success, %FALSE if an error occurred */ gboolean udisks_daemon_util_file_set_contents (const gchar *filename, const gchar *contents, gssize contents_len, gint mode_for_new_file, GError **error) { gboolean ret; struct stat statbuf; gint mode; gchar *tmpl; gint fd; FILE *f; ret = FALSE; tmpl = NULL; if (stat (filename, &statbuf) != 0) { if (errno == ENOENT) { mode = mode_for_new_file; } else { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Error stat(2)'ing %s: %m", filename); goto out; } } else { mode = statbuf.st_mode; } tmpl = g_strdup_printf ("%s.XXXXXX", filename); fd = g_mkstemp_full (tmpl, O_RDWR, mode); if (fd == -1) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Error creating temporary file: %m"); goto out; } f = fdopen (fd, "w"); if (f == NULL) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Error calling fdopen: %m"); g_unlink (tmpl); goto out; } if (contents_len < 0 ) contents_len = strlen (contents); if (fwrite (contents, 1, contents_len, f) != (gsize) contents_len) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Error calling fwrite on temp file: %m"); fclose (f); g_unlink (tmpl); goto out; } if (fsync (fileno (f)) != 0) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Error calling fsync on temp file: %m"); fclose (f); g_unlink (tmpl); goto out; } fclose (f); if (rename (tmpl, filename) != 0) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Error renaming temp file to final file: %m"); g_unlink (tmpl); goto out; } ret = TRUE; out: g_free (tmpl); return ret; } /* ---------------------------------------------------------------------------------------------------- */ /** * UDisksInhibitCookie: * * Opaque data structure used in udisks_daemon_util_inhibit_system_sync() and * udisks_daemon_util_uninhibit_system_sync(). */ struct UDisksInhibitCookie { /*< private >*/ guint32 magic; #ifdef HAVE_LIBSYSTEMD_LOGIN gint fd; #endif }; /** * udisks_daemon_util_inhibit_system_sync: * @reason: A human readable explanation of why the system is being inhibited. * * Tries to inhibit the system. * * Right now only * systemd * inhibitors are supported but other inhibitors can be added in the future. * * Returns: A cookie that can be used with udisks_daemon_util_uninhibit_system_sync(). */ UDisksInhibitCookie * udisks_daemon_util_inhibit_system_sync (const gchar *reason) { #ifdef HAVE_LIBSYSTEMD_LOGIN UDisksInhibitCookie *ret = NULL; GDBusConnection *connection = NULL; GVariant *value = NULL; GUnixFDList *fd_list = NULL; gint32 index = -1; GError *error = NULL; g_return_val_if_fail (reason != NULL, NULL); connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); if (connection == NULL) { udisks_critical ("Error getting system bus: %s (%s, %d)", error->message, g_quark_to_string (error->domain), error->code); g_clear_error (&error); goto out; } value = g_dbus_connection_call_with_unix_fd_list_sync (connection, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "Inhibit", g_variant_new ("(ssss)", "sleep:shutdown:idle", /* what */ "Disk Manager", /* who */ reason, /* why */ "block"), /* mode */ G_VARIANT_TYPE ("(h)"), G_DBUS_CALL_FLAGS_NONE, -1, /* default timeout */ NULL, /* fd_list */ &fd_list, /* out_fd_list */ NULL, /* GCancellable */ &error); if (value == NULL) { udisks_critical ("Error inhibiting: %s (%s, %d)", error->message, g_quark_to_string (error->domain), error->code); g_clear_error (&error); goto out; } g_variant_get (value, "(h)", &index); g_assert (index >= 0 && index < g_unix_fd_list_get_length (fd_list)); ret = g_new0 (UDisksInhibitCookie, 1); ret->magic = 0xdeadbeef; ret->fd = g_unix_fd_list_get (fd_list, index, &error); if (ret->fd == -1) { udisks_critical ("Error getting fd: %s (%s, %d)", error->message, g_quark_to_string (error->domain), error->code); g_clear_error (&error); g_free (ret); ret = NULL; goto out; } out: if (value != NULL) g_variant_unref (value); g_clear_object (&fd_list); g_clear_object (&connection); return ret; #else /* non-systemd: just return a dummy pointer */ g_return_val_if_fail (reason != NULL, NULL); return (UDisksInhibitCookie* ) &udisks_daemon_util_inhibit_system_sync; #endif } /** * udisks_daemon_util_uninhibit_system_sync: * @cookie: %NULL or a cookie obtained from udisks_daemon_util_inhibit_system_sync(). * * Does nothing if @cookie is %NULL, otherwise uninhibits. */ void udisks_daemon_util_uninhibit_system_sync (UDisksInhibitCookie *cookie) { #ifdef HAVE_LIBSYSTEMD_LOGIN if (cookie != NULL) { g_assert (cookie->magic == 0xdeadbeef); if (close (cookie->fd) != 0) { udisks_critical ("Error closing inhibit-fd: %m"); } g_free (cookie); } #else /* non-systemd: check dummy pointer */ g_warn_if_fail (cookie == (UDisksInhibitCookie* ) &udisks_daemon_util_inhibit_system_sync); #endif } /** * udisks_daemon_util_get_free_mdraid_device: * * Gets a free MD RAID device. * * Returns: A string of the form "/dev/mdNNN" that should be freed * with g_free() or %NULL if no free device is available. */ gchar * udisks_daemon_util_get_free_mdraid_device (void) { gchar *ret = NULL; gint n; gchar buf[PATH_MAX]; /* Ideally we wouldn't need this racy function... but mdadm(8) * insists that the user chooses a name. It should just choose one * itself but that's not how things work right now. */ for (n = 127; n >= 0; n--) { snprintf (buf, sizeof buf, "/sys/block/md%d", n); if (!g_file_test (buf, G_FILE_TEST_EXISTS)) { ret = g_strdup_printf ("/dev/md%d", n); goto out; } } out: return ret; } /** * udisks_ata_identify_get_word: * @identify_data: (allow-none): A 512-byte array containing ATA IDENTIFY or ATA IDENTIFY PACKET DEVICE data or %NULL. * @word_number: The word number to get - must be less than 256. * * Gets a word from position @word_number from * @identify_data. * * Returns: The word at the specified position or 0 if @identify_data is %NULL. */ guint16 udisks_ata_identify_get_word (const guchar *identify_data, guint word_number) { const guint16 *words = (const guint16 *) identify_data; guint16 ret = 0; g_return_val_if_fail (word_number < 256, 0); if (identify_data == NULL) goto out; ret = GUINT16_FROM_LE (words[word_number]); out: return ret; } /* ---------------------------------------------------------------------------------------------------- */ static volatile guint uevent_serial = 0; static gboolean trigger_uevent (const gchar *path, const gchar *str) { gint fd; fd = open (path, O_WRONLY); if (fd < 0) { udisks_warning ("Error opening %s while triggering uevent: %m", path); return FALSE; } if (write (fd, str, strlen (str)) != (ssize_t) strlen (str)) { udisks_warning ("Error writing '%s' to file %s: %m", str, path); close (fd); return FALSE; } close (fd); return TRUE; } typedef struct { UDisksDaemon *daemon; GMainLoop *main_loop; guint serial; gchar *uevent_path; gboolean success; } SynthUeventData; static gboolean trigger_uevent_idle_cb (gpointer user_data) { SynthUeventData *data = user_data; gchar *str; str = g_strdup_printf ("change %s UDISKSSERIAL=%u", udisks_daemon_get_uuid (data->daemon), data->serial); if (! trigger_uevent (data->uevent_path, str)) { /* kernel refused our string, try simple "change" but don't wait for it */ trigger_uevent (data->uevent_path, "change"); data->success = FALSE; g_main_loop_quit (data->main_loop); } g_free (str); /* remove the source */ return FALSE; } static gboolean uevent_wait_timeout_cb (gpointer user_data) { SynthUeventData *data = user_data; data->success = FALSE; g_main_loop_quit (data->main_loop); /* remove the source */ return FALSE; } static void uevent_probed_cb (UDisksLinuxProvider *provider, const gchar *action, UDisksLinuxDevice *device, gpointer user_data) { SynthUeventData *data = user_data; const gchar *received_serial_str; gint64 received_serial; gchar *endptr; received_serial_str = g_udev_device_get_property (device->udev_device, "SYNTH_ARG_UDISKSSERIAL"); if (received_serial_str != NULL) { endptr = (gchar *) received_serial_str; received_serial = g_ascii_strtoll (received_serial_str, &endptr, 0); if (endptr != received_serial_str && received_serial == data->serial) { data->success = TRUE; g_main_loop_quit (data->main_loop); } } } static gchar * resolve_uevent_path (UDisksDaemon *daemon, const gchar *device_file, const gchar *sysfs_path) { GUdevClient *gudev_client; GUdevDevice *gudev_device; gchar *path = NULL; gchar *basename; if (sysfs_path != NULL) return g_build_filename (sysfs_path, "uevent", NULL); /* try querying the udev database */ gudev_client = udisks_linux_provider_get_udev_client (udisks_daemon_get_linux_provider (daemon)); /* gudev calls stat() on the device_file, effectively resolving symlinks */ gudev_device = g_udev_client_query_by_device_file (gudev_client, device_file); if (gudev_device != NULL) { path = g_build_filename (g_udev_device_get_sysfs_path (gudev_device), "uevent", NULL); g_object_unref (gudev_device); } if (path != NULL) return path; /* construct the path manually, assuming no entries in /dev */ basename = g_path_get_basename (device_file); path = g_build_filename ("/sys/block", basename, "uevent", NULL); g_free (basename); return path; } /** * udisks_daemon_util_trigger_uevent: * @daemon: A #UDisksDaemon. * @device_file: Block device file (/dev/xxx) or %NULL. * @sysfs_path: Device path in /sys or %NULL. * * Triggers a 'change' uevent in the kernel. * * The @sysfs_path takes precedence if non-NULL over a @device_file. * In case of using @device_file any symlinks are resolved, this expects * the block device has been processed by udev already. * * The triggered event will bubble up from the kernel through the udev * stack and will eventually be received by the udisks daemon process * itself. This method does not wait for the event to be received. */ void udisks_daemon_util_trigger_uevent (UDisksDaemon *daemon, const gchar *device_file, const gchar *sysfs_path) { gchar *path; g_return_if_fail (UDISKS_IS_DAEMON (daemon)); g_return_if_fail (device_file != NULL || sysfs_path != NULL); path = resolve_uevent_path (daemon, device_file, sysfs_path); trigger_uevent (path, "change"); g_free (path); } /** * udisks_daemon_util_trigger_uevent_sync: * @daemon: A #UDisksDaemon. * @device_file: Block device file (/dev/xxx) or %NULL. * @sysfs_path: Device path in /sys or %NULL. * @timeout_seconds: Maximum time to wait for the uevent (in seconds). * * Triggers a 'change' uevent in the kernel and waits until it's received and * processed by udisks. * * The @sysfs_path takes precedence if non-NULL over a @device_file. * In case of using @device_file any symlinks are resolved, this expects * the block device has been processed by udev already. * * Unlike udisks_daemon_util_trigger_uevent() that just triggers * a synthetic uevent to the kernel, this call will actually block and wait until * the #UDisksLinuxProvider receives the uevent, performs probing and processes * the uevent further down the UDisks object stack. Upon returning from this * function call the caller may assume the event has been fully processed, all * D-Bus objects are updated and settled. Typically used in busy wait for * a particular D-Bus interface. * * Note that this uses synthetic uevent tagging and only works on linux kernel * 4.13 and higher. In case an older kernel is detected this acts like the classic * udisks_daemon_util_trigger_uevent() call and %FALSE is returned. * * Returns: %TRUE if the uevent has been successfully received, %FALSE otherwise * or when the kernel version is too old. */ gboolean udisks_daemon_util_trigger_uevent_sync (UDisksDaemon *daemon, const gchar *device_file, const gchar *sysfs_path, guint timeout_seconds) { UDisksLinuxProvider *provider; SynthUeventData data; GMainContext *main_context; GSource *idle_source; GSource *timeout_source; g_return_val_if_fail (UDISKS_IS_DAEMON (daemon), FALSE); g_return_val_if_fail (device_file != NULL || sysfs_path != NULL, FALSE); if (bd_utils_check_linux_version (4, 13, 0) < 0) { udisks_daemon_util_trigger_uevent (daemon, device_file, sysfs_path); return FALSE; } data.daemon = daemon; data.uevent_path = resolve_uevent_path (daemon, device_file, sysfs_path); if (data.uevent_path == NULL) return FALSE; data.serial = g_atomic_int_add (&uevent_serial, 1); main_context = g_main_context_new (); g_main_context_push_thread_default (main_context); data.main_loop = g_main_loop_new (main_context, FALSE); /* queue the actual trigger in the loop */ idle_source = g_idle_source_new (); g_source_set_callback (idle_source, (GSourceFunc) trigger_uevent_idle_cb, &data, NULL); g_source_attach (idle_source, main_context); g_source_unref (idle_source); /* add timeout as a fallback */ timeout_source = g_timeout_source_new_seconds (timeout_seconds); g_source_set_callback (timeout_source, (GSourceFunc) uevent_wait_timeout_cb, &data, NULL); g_source_attach (timeout_source, main_context); g_source_unref (timeout_source); /* catch incoming uevents */ provider = udisks_daemon_get_linux_provider (daemon); g_signal_connect (provider, "uevent-probed", G_CALLBACK (uevent_probed_cb), &data); data.success = FALSE; g_main_loop_run (data.main_loop); g_signal_handlers_disconnect_by_func (provider, uevent_probed_cb, &data); g_main_context_pop_thread_default (main_context); g_main_loop_unref (data.main_loop); g_main_context_unref (main_context); g_free (data.uevent_path); return data.success; } /* ---------------------------------------------------------------------------------------------------- */