diff --git a/Makefile.am b/Makefile.am index 4948d2b6..b354e0bd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -45,6 +45,7 @@ AM_CPPFLAGS = \ -DFLATPAK_BASEDIR=\"$(pkgdatadir)\" \ -DFLATPAK_TRIGGERDIR=\"$(pkgdatadir)/triggers\" \ -DSYSTEM_FONTS_DIR=\"$(SYSTEM_FONTS_DIR)\" \ + -DSYSTEM_HELPER_USER=\"$(SYSTEM_HELPER_USER)\" \ -DSYSTEM_FONT_CACHE_DIRS=\"$(SYSTEM_FONT_CACHE_DIRS)\" \ -DG_LOG_DOMAIN=\"flatpak\" \ -I$(srcdir)/libglnx \ diff --git a/common/flatpak-dir-private.h b/common/flatpak-dir-private.h index b083eba0..5b0bdce1 100644 --- a/common/flatpak-dir-private.h +++ b/common/flatpak-dir-private.h @@ -207,6 +207,13 @@ typedef enum { #define FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_ALL (FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_NO_INTERACTION) +typedef enum { + FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_NONE = 0, + FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_NO_INTERACTION = 1 << 0, +} FlatpakHelperGetRevokefsFdFlags; + +#define FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_ALL (FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_NO_INTERACTION) + typedef enum { FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_NONE = 0, FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_NO_INTERACTION = 1 << 0, diff --git a/common/flatpak-dir.c b/common/flatpak-dir.c index 4dee35ae..3be6e047 100644 --- a/common/flatpak-dir.c +++ b/common/flatpak-dir.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -1154,6 +1155,64 @@ flatpak_dir_remove_oci_files (FlatpakDir *self, return TRUE; } +static gchar * +flatpak_dir_revokefs_fuse_create_mountpoint (const gchar *ref, + GError **error) +{ + g_autoptr(GFile) cache_dir = NULL; + g_auto(GStrv) parts = NULL; + g_autofree gchar *cache_dir_path = NULL; + g_autofree gchar *mnt_dir = NULL; + g_autofree gchar *mountpoint = NULL; + + cache_dir = flatpak_ensure_system_user_cache_dir_location (error); + if (cache_dir == NULL) + return NULL; + + parts = flatpak_decompose_ref (ref, error); + if (parts == NULL) + return NULL; + cache_dir_path = g_file_get_path (cache_dir); + mnt_dir = g_strdup_printf ("%s-XXXXXX", parts[1]); + mountpoint = g_mkdtemp_full (g_build_filename (cache_dir_path, mnt_dir, NULL), 0755); + if (mountpoint == NULL) + { + glnx_set_error_from_errno (error); + return NULL; + } + + return g_steal_pointer (&mountpoint); +} + +static gboolean +flatpak_dir_revokefs_fuse_unmount (OstreeRepo **repo, + GLnxLockFile *lockfile, + const gchar *mnt_dir, + GError **error) +{ + g_autoptr(GSubprocess) fusermount = NULL; + + /* Clear references to child_repo as not to leave any open fds. This is needed for + * a clean umount operation. + */ + g_clear_pointer (repo, g_object_unref); + glnx_release_lock_file (lockfile); + + fusermount = g_subprocess_new (G_SUBPROCESS_FLAGS_NONE, + error, + "fusermount", "-u", "-z", mnt_dir, + NULL); + if (g_subprocess_wait_check (fusermount, NULL, error)) + { + g_autoptr(GFile) mnt_dir_file = g_file_new_for_path (mnt_dir); + + flatpak_rm_rf (mnt_dir_file, NULL, NULL); + return TRUE; + } + + return FALSE; +} + static gboolean flatpak_dir_use_system_helper (FlatpakDir *self, const char *installing_from_remote) @@ -1487,6 +1546,46 @@ flatpak_dir_system_helper_call_ensure_repo (FlatpakDir *self, return ret != NULL; } +static gboolean +flatpak_dir_system_helper_call_get_revokefs_fd (FlatpakDir *self, + guint arg_flags, + const gchar *arg_installation, + gint *out_socket, + gchar **out_src_dir, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GUnixFDList) out_fd_list = NULL; + gint fd = -1; + gint fd_index = -1; + + if (flatpak_dir_get_no_interaction (self)) + arg_flags |= FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_NO_INTERACTION; + + g_debug ("Calling system helper: GetRevokefsFd"); + + g_autoptr(GVariant) ret = + flatpak_dir_system_helper_call (self, "GetRevokefsFd", + g_variant_new ("(us)", + arg_flags, + arg_installation), + G_VARIANT_TYPE ("(hs)"), + &out_fd_list, + cancellable, error); + + if (ret == NULL) + return FALSE; + + g_variant_get (ret, "(hs)", &fd_index, out_src_dir); + fd = g_unix_fd_list_get (out_fd_list, fd_index, error); + if (fd == -1) + return FALSE; + + *out_socket = fd; + + return TRUE; +} + static gboolean flatpak_dir_system_helper_call_update_summary (FlatpakDir *self, guint arg_flags, @@ -7702,6 +7801,80 @@ flatpak_dir_create_system_child_repo (FlatpakDir *self, return flatpak_dir_create_child_repo (self, cache_dir, file_lock, optional_commit, error); } +static gboolean +flatpak_dir_setup_revokefs_fuse_mount (FlatpakDir *self, + const gchar *ref, + const gchar *installation, + gchar **out_src_dir, + gchar **out_mnt_dir, + GCancellable *cancellable) +{ + g_autoptr (GError) local_error = NULL; + g_autofree gchar *src_dir_tmp = NULL; + g_autofree gchar *mnt_dir_tmp = NULL; + gint socket = -1; + gboolean res = FALSE; + + if (!flatpak_dir_system_helper_call_get_revokefs_fd (self, + FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_NONE, + installation ? installation : "", + &socket, + &src_dir_tmp, + cancellable, + &local_error)) + { + if (g_error_matches (local_error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED)) + g_debug ("revokefs-fuse not supported on your installation: %s", local_error->message); + else + g_warning ("Failed to get revokefs-fuse socket from system-helper: %s", local_error->message); + + goto out; + } + else + { + g_autoptr(GSubprocess) revokefs_fuse = NULL; + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autofree gchar *client_uid = NULL; + + mnt_dir_tmp = flatpak_dir_revokefs_fuse_create_mountpoint (ref, &local_error); + if (mnt_dir_tmp == NULL) + { + g_warning ("Failed to create a mountpoint for revokefs-fuse: %s", local_error->message); + close (socket); + goto out; + } + + client_uid = g_strdup_printf ("uid=%d", getuid ()); + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_take_fd (launcher, socket, 3); + revokefs_fuse = g_subprocess_launcher_spawn (launcher, + &local_error, + "revokefs-fuse", "-o", client_uid, "--socket=3", + src_dir_tmp, mnt_dir_tmp, NULL); + if (revokefs_fuse == NULL || + !g_subprocess_wait_check (revokefs_fuse, NULL, &local_error)) + { + g_warning ("Error spawning revokefs-fuse: %s", local_error->message); + close (socket); + goto out; + } + } + + res = TRUE; + +out: + /* It is unconventional to steal these values on error. However, it depends on where + * this function failed. If we are able to spawn the revokefs backend (src_dir_tmp + * is non-NULL) but failed to create mountpoint or spawning revokefs-fuse here, + * we still need the src_dir_tmp value to cleanup the revokefs backend properly + * through the system-helper's CancelPull(). Hence, always stealing values can tell + * the caller under what circumstances this function failed and cleanup accordingly. */ + *out_mnt_dir = g_steal_pointer (&mnt_dir_tmp); + *out_src_dir = g_steal_pointer (&src_dir_tmp); + + return res; +} + gboolean flatpak_dir_install (FlatpakDir *self, gboolean no_pull, @@ -7795,13 +7968,71 @@ flatpak_dir_install (FlatpakDir *self, } else { - /* We're pulling from a remote source, we do the network mirroring pull as a - user and hand back the resulting data to the system-helper, that trusts us - due to the GPG signatures in the repo */ + /* For system pulls, the pull has to be made in a child repo first, + which is then pulled into the system's one. The pull from child + repo into the system repo can occur in one of the two following ways: + 1) Hard-link the child repo into system's one. + 2) Copy and verify each object from the child repo to the system's one. - child_repo = flatpak_dir_create_system_child_repo (self, &child_repo_lock, NULL, error); - if (child_repo == NULL) - return FALSE; + 2) poses the problem of using double disk-space which might fail the + installation of very big applications. For e.g. at endless, the encyclopedia app + is about ~6GB, hence ~12GB of free disk-space is required to get it installed. + + For 1), we need to make sure that we address all the security concerns that + might escalate during the pull from a remote into child repo and subsequently, + hard-linking it into the (root-owned)system repo. This is taken care of by a + special FUSE process(revokefs-fuse) which guards all the writes made to the + child repo and ensures that no file descriptors remain open to the child repo + before the hard-linkable pull is made into the system's repo. + More details about the security issues dealt here are present at + https://github.com/flatpak/flatpak/wiki/Noncopying-system-app-installation + + In case we fail to apply pull approach 1), the pull automatically fallbacks to use 2). */ + g_autofree gchar *src_dir = NULL; + g_autofree gchar *mnt_dir = NULL; + gboolean is_revokefs_pull = FALSE; + g_autoptr(GError) local_error = NULL; + + if (!flatpak_dir_setup_revokefs_fuse_mount (self, + ref, + installation, + &src_dir, &mnt_dir, + cancellable)) + { + /* Error handling for failure */ + } + else + { + g_autofree gchar *repo_basename = NULL; + g_autoptr(GFile) mnt_dir_file = NULL; + + mnt_dir_file = g_file_new_for_path (mnt_dir); + child_repo = flatpak_dir_create_child_repo (self, mnt_dir_file, &child_repo_lock, opt_commit, &local_error); + if (child_repo == NULL) + { + g_warning ("Cannot create repo on revokefs mountpoint %s: %s", mnt_dir, local_error->message); + } + else + { + repo_basename = g_file_get_basename (ostree_repo_get_path (child_repo)); + child_repo_path = g_build_filename (src_dir, repo_basename, NULL); + is_revokefs_pull = TRUE; + } + } + + /* Fallback if revokefs-fuse setup does not succeed. This makes the pull + * temporarily use double disk-space. */ + if (!is_revokefs_pull) + { + /* We're pulling from a remote source, we do the network mirroring pull as a + user and hand back the resulting data to the system-helper, that trusts us + due to the GPG signatures in the repo */ + child_repo = flatpak_dir_create_system_child_repo (self, &child_repo_lock, NULL, error); + if (child_repo == NULL) + return FALSE; + else + child_repo_path = g_file_get_path (ostree_repo_get_path (child_repo)); + } flatpak_flags |= FLATPAK_PULL_FLAGS_SIDELOAD_EXTRA_DATA; @@ -7815,7 +8046,14 @@ flatpak_dir_install (FlatpakDir *self, if (!child_repo_ensure_summary (child_repo, state, cancellable, error)) return FALSE; - child_repo_path = g_file_get_path (ostree_repo_get_path (child_repo)); + g_assert (child_repo_path != NULL); + + if (is_revokefs_pull && + !flatpak_dir_revokefs_fuse_unmount (&child_repo, &child_repo_lock, mnt_dir, &local_error)) + { + g_warning ("Could not unmount revokefs-fuse filesystem at %s: %s", mnt_dir, local_error->message); + return FALSE; + } } if (no_deploy) diff --git a/configure.ac b/configure.ac index caebbfe2..ca49ceb1 100644 --- a/configure.ac +++ b/configure.ac @@ -312,6 +312,13 @@ AC_ARG_WITH(system-install-dir, SYSTEM_INSTALL_DIR=$with_system_install_dir AC_SUBST(SYSTEM_INSTALL_DIR) +AC_ARG_WITH(system-helper-user, + [AS_HELP_STRING([--with-system-helper-user=USERNAME], + [Name of the system helper user])], + with_system_helper_user="$withval", with_system_helper_user=flatpak) +SYSTEM_HELPER_USER=$with_system_helper_user +AC_SUBST(SYSTEM_HELPER_USER) + AC_ARG_ENABLE(documentation, AC_HELP_STRING([--enable-documentation], [Build documentation]),, enable_documentation=yes) diff --git a/data/org.freedesktop.Flatpak.xml b/data/org.freedesktop.Flatpak.xml index de7b7eca..9590592d 100644 --- a/data/org.freedesktop.Flatpak.xml +++ b/data/org.freedesktop.Flatpak.xml @@ -152,6 +152,13 @@ + + + + + + + diff --git a/system-helper/flatpak-system-helper.c b/system-helper/flatpak-system-helper.c index 34c81585..9572e742 100644 --- a/system-helper/flatpak-system-helper.c +++ b/system-helper/flatpak-system-helper.c @@ -26,6 +26,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "flatpak-dbus-generated.h" #include "flatpak-dir-private.h" @@ -37,6 +43,9 @@ static FlatpakSystemHelper *helper = NULL; static GMainLoop *main_loop = NULL; static guint name_owner_id = 0; +G_LOCK_DEFINE (cache_dirs_in_use); +static GHashTable *cache_dirs_in_use = NULL; + static gboolean on_session_bus = FALSE; static gboolean no_idle_exit = FALSE; @@ -52,6 +61,80 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitAuthorizationResult, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitDetails, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitSubject, g_object_unref) +typedef struct +{ + FlatpakSystemHelper *object; + GDBusMethodInvocation *invocation; + GCancellable *cancellable; + + guint watch_id; + uid_t uid; /* uid of the client initiating the pull */ + + gint client_socket; /* fd that is send back to the client for spawning revoke-fuse */ + + gchar *src_dir; /* source directory containing the actual child repo */ + gchar *unique_name; + + GSubprocess *revokefs_backend; +} OngoingPull; + +static void +terminate_revokefs_backend (OngoingPull *pull) +{ + /* Terminating will guarantee that all access to write operations are revoked. */ + if (shutdown (pull->client_socket, SHUT_RDWR) == -1 || + !g_subprocess_wait (pull->revokefs_backend, NULL, NULL)) + { + g_warning ("Failed to shutdown client socket, killing backend writer process"); + g_subprocess_force_exit (pull->revokefs_backend); + } + + g_clear_object (&pull->revokefs_backend); +} + +static gboolean +remove_dir_from_cache_dirs_in_use (const char *src_dir) +{ + gboolean res; + + G_LOCK (cache_dirs_in_use); + res = g_hash_table_remove (cache_dirs_in_use, src_dir); + G_UNLOCK (cache_dirs_in_use); + + return res; +} + +static void +ongoing_pull_free (OngoingPull *pull) +{ + g_autoptr(GFile) src_dir_file = NULL; + g_autoptr(GError) local_error = NULL; + + g_clear_handle_id (&pull->watch_id, g_bus_unwatch_name); + + src_dir_file = g_file_new_for_path (pull->src_dir); + + if (pull->revokefs_backend) + terminate_revokefs_backend (pull); + + if (!flatpak_rm_rf (src_dir_file, NULL, &local_error)) + { + g_warning ("Unable to remove ongoing pull's src dir at %s: %s", + pull->src_dir, local_error->message); + g_clear_error (&local_error); + } + + remove_dir_from_cache_dirs_in_use (pull->src_dir); + + g_clear_pointer (&pull->src_dir, g_free); + g_clear_pointer (&pull->unique_name, g_free); + close (pull->client_socket); + + g_slice_free (OngoingPull, pull); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OngoingPull, ongoing_pull_free); + static void skeleton_died_cb (gpointer data) { @@ -92,6 +175,12 @@ unref_skeleton_in_timeout (void) static gboolean idle_timeout_cb (gpointer user_data) { + G_LOCK (cache_dirs_in_use); + guint ongoing_pulls_len = g_hash_table_size (cache_dirs_in_use); + G_UNLOCK (cache_dirs_in_use); + if (ongoing_pulls_len != 0) + return G_SOURCE_CONTINUE; + if (name_owner_id) { g_debug ("Idle - unowning name"); @@ -217,6 +306,62 @@ flatpak_invocation_return_error (GDBusMethodInvocation *invocation, } } +static gboolean +get_connection_uid (GDBusMethodInvocation *invocation, uid_t *out_uid, GError **error) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + g_autoptr(GVariant) dict = NULL; + g_autoptr(GVariant) credentials = NULL; + + credentials = g_dbus_connection_call_sync (connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetConnectionCredentials", + g_variant_new ("(s)", sender), + G_VARIANT_TYPE ("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, NULL, error); + if (credentials == NULL) + return FALSE; + + dict = g_variant_get_child_value (credentials, 0); + + if (!g_variant_lookup (dict, "UnixUserID", "u", out_uid)) + { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Failed to query UnixUserID for the bus name: %s", sender); + return FALSE; + } + + return TRUE; +} + +static OngoingPull * +take_ongoing_pull_by_dir (const gchar *src_dir) +{ + OngoingPull *pull = NULL; + gpointer key, value; + + G_LOCK (cache_dirs_in_use); + /* Keep src_dir key inside hashtable but remove its OngoingPull + * value and set it to NULL. This way src_dir is still marked + * as in-use (as Deploy or CancelPull might be executing on it, + * whereas OngoingPull ownership is transferred to respective + * callers. */ + if (g_hash_table_steal_extended (cache_dirs_in_use, src_dir, &key, &value)) + { + if (value) + { + g_hash_table_insert (cache_dirs_in_use, key, NULL); + pull = value; + } + } + G_UNLOCK (cache_dirs_in_use); + + return pull; +} + static gboolean handle_deploy (FlatpakSystemHelper *object, GDBusMethodInvocation *invocation, @@ -237,6 +382,8 @@ handle_deploy (FlatpakSystemHelper *object, gboolean local_pull; gboolean reinstall; g_autofree char *url = NULL; + g_autoptr(OngoingPull) ongoing_pull = NULL; + g_autofree gchar *src_dir = NULL; g_debug ("Deploy %s %u %s %s %s", arg_repo_path, arg_flags, arg_ref, arg_origin, arg_installation); @@ -247,6 +394,49 @@ handle_deploy (FlatpakSystemHelper *object, return TRUE; } + src_dir = g_path_get_dirname (arg_repo_path); + ongoing_pull = take_ongoing_pull_by_dir (src_dir); + if (ongoing_pull != NULL) + { + g_autoptr(GError) local_error = NULL; + uid_t uid; + + /* Ensure that pull's uid is same as the caller's uid */ + if (!get_connection_uid (invocation, &uid, &local_error)) + { + g_dbus_method_invocation_return_gerror (invocation, local_error); + return TRUE; + } + else + { + if (ongoing_pull->uid != uid) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Ongoing pull's uid(%d) does not match with peer uid(%d)", + ongoing_pull->uid, uid); + return TRUE; + } + } + + terminate_revokefs_backend (ongoing_pull); + + if (!flatpak_canonicalize_permissions (AT_FDCWD, + arg_repo_path, + getuid() == 0 ? 0 : -1, + getuid() == 0 ? 0 : -1, + &local_error)) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Failed to canonicalize permissions of repo %s: %s", + arg_repo_path, local_error->message); + return TRUE; + } + + /* At this point, the cache-dir's repo is owned by root. Hence, any failure + * from here on, should always cleanup the cache-dir and not preserve it to be re-used. */ + ongoing_pull->preserve_pull = FALSE; + } + if ((arg_flags & ~FLATPAK_HELPER_DEPLOY_FLAGS_ALL) != 0) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, @@ -1150,6 +1340,316 @@ handle_run_triggers (FlatpakSystemHelper *object, return TRUE; } +static gboolean +check_for_system_helper_user (struct passwd *passwd, + gchar **passwd_buf, + GError **error) +{ + struct passwd *result = NULL; + g_autofree gchar *buf = NULL; + size_t bufsize; + int err; + + bufsize = sysconf (_SC_GETPW_R_SIZE_MAX); + if (bufsize == -1) /* Value was indeterminate */ + bufsize = 16384; /* Should be more than enough */ + + while (!result) + { + buf = g_malloc0 (bufsize); + err = getpwnam_r (SYSTEM_HELPER_USER, passwd, buf, bufsize, &result); + if (result == NULL) + { + if (err == ERANGE) /* Insufficient buffer space */ + { + g_free (buf); + bufsize *= 2; + continue; + } + else if (err == 0) /* User SYSTEM_HELPER_USER 's record was not found*/ + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "User %s does not exist in password file entry", SYSTEM_HELPER_USER); + return FALSE; + } + else + { + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (err), + "Failed to query user %s from password file entry", SYSTEM_HELPER_USER); + return FALSE; + } + } + } + + *passwd_buf = g_steal_pointer (&buf); + + return TRUE; +} + +static void +revokefs_fuse_backend_child_setup (gpointer user_data) +{ + struct passwd *passwd = user_data; + + if (setgid (passwd->pw_gid) == -1) + { + g_warning ("Failed to setgid(%d) for revokefs backend: %s", + passwd->pw_gid, g_strerror (errno)); + exit (1); + } + + if (setuid (passwd->pw_uid) == -1) + { + g_warning ("Failed to setuid(%d) for revokefs backend: %s", + passwd->pw_uid, g_strerror (errno)); + exit (1); + } +} + +static void +name_vanished_cb (GDBusConnection *connection, const gchar *name, gpointer user_data) +{ + const gchar *unique_name = (const gchar *) user_data; + g_autoptr(GPtrArray) cleanup_pulls = NULL; + GHashTableIter iter; + gpointer value; + + cleanup_pulls = g_ptr_array_new_with_free_func ((GDestroyNotify) ongoing_pull_free); + + G_LOCK (cache_dirs_in_use); + g_hash_table_iter_init (&iter, cache_dirs_in_use); + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + OngoingPull *pull = (OngoingPull *) value; + if (g_strcmp0 (pull->unique_name, unique_name) == 0) + { + g_ptr_array_add (cleanup_pulls, pull); + g_hash_table_iter_remove (&iter); + } + } + G_UNLOCK (cache_dirs_in_use); +} + +static OngoingPull * +ongoing_pull_new (FlatpakSystemHelper *object, + GDBusMethodInvocation *invocation, + struct passwd *passwd, + uid_t uid, + const gchar *src, + GError **error) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + g_autoptr(OngoingPull) pull = NULL; + g_autoptr(GSubprocessLauncher) launcher = NULL; + int sockets[2]; + + pull = g_slice_new0 (OngoingPull); + pull->object = object; + pull->invocation = invocation; + pull->src_dir = g_strdup (src); + pull->cancellable = g_cancellable_new (); + pull->uid = uid; + pull->unique_name = g_strdup (g_dbus_connection_get_unique_name (connection)); + + pull->watch_id = g_bus_watch_name_on_connection (connection, + pull->unique_name, + G_BUS_NAME_WATCHER_FLAGS_NONE, NULL, + name_vanished_cb, + g_strdup (g_dbus_connection_get_unique_name (connection)), + g_free); + + if (socketpair (AF_UNIX, SOCK_SEQPACKET, 0, sockets) == -1) + { + glnx_throw_errno_prefix (error, "Failed to get a socketpair"); + return NULL; + } + + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_set_child_setup (launcher, revokefs_fuse_backend_child_setup, passwd, NULL); + g_subprocess_launcher_take_fd (launcher, sockets[0], 3); + fcntl (sockets[1], F_SETFD, FD_CLOEXEC); + pull->client_socket = sockets[1]; + + pull->revokefs_backend = g_subprocess_launcher_spawn (launcher, + error, + "revokefs-fuse", + "--backend", + "--socket=3", src, NULL); + if (pull->revokefs_backend == NULL) + return NULL; + + return g_steal_pointer (&pull); +} + +static gboolean +reuse_cache_dir_if_available (const gchar *repo_tmp, + gchar **out_src_dir, + struct passwd *passwd) +{ + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GFile) repo_tmpfile = NULL; + GFileInfo *file_info = NULL; + g_autoptr(GError) error = NULL; + const gchar *name; + gboolean res = FALSE; + + g_debug ("Checking for any temporary cache directory available to reuse"); + + repo_tmpfile = g_file_new_for_path (repo_tmp); + enumerator = g_file_enumerate_children (repo_tmpfile, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, &error); + if (enumerator == NULL) + { + g_warning ("Failed to enumerate %s: %s", repo_tmp, error->message); + return FALSE; + } + + while (TRUE) + { + if (!g_file_enumerator_iterate (enumerator, &file_info, NULL, NULL, &error)) + { + g_warning ("Error while iterating %s: %s", repo_tmp, error->message); + break; + } + + if (file_info == NULL || res == TRUE) + break; + + name = g_file_info_get_name (file_info); + if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY && + g_str_has_prefix (name, "flatpak-cache-")) + { + g_autoptr(GFile) cache_dir_file = g_file_get_child (repo_tmpfile, name); + g_autofree gchar *cache_dir_name = g_file_get_path (cache_dir_file); + + G_LOCK (cache_dirs_in_use); + if (!g_hash_table_contains (cache_dirs_in_use, cache_dir_name)) + { + struct stat st_buf; + + /* We are able to find a cache dir which is not in use. */ + if (stat (cache_dir_name, &st_buf) == 0 && + st_buf.st_uid == passwd->pw_uid && /* should be owned by SYSTEM_HELPER_USER */ + (st_buf.st_mode & 0022) == 0) /* should not be world-writeable */ + { + gboolean did_not_exist = g_hash_table_insert (cache_dirs_in_use, + g_strdup (cache_dir_name), + NULL); + g_assert (did_not_exist); + *out_src_dir = g_steal_pointer (&cache_dir_name); + res = TRUE; + } + } + G_UNLOCK (cache_dirs_in_use); + } + } + + return res; +} + +static gboolean +handle_get_revokefs_fd (FlatpakSystemHelper *object, + GDBusMethodInvocation *invocation, + GUnixFDList *arg_fdlist, + guint arg_flags, + const gchar *arg_installation) +{ + g_autoptr(FlatpakDir) system = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *src_dir = NULL; + g_autofree gchar *flatpak_dir = NULL; + g_autofree gchar *repo_tmp = NULL; + g_autofree gchar *passwd_buf = NULL; + struct passwd passwd; + OngoingPull *new_pull; + uid_t uid; + int fd_index = -1; + + g_debug ("GetRevokefsFd %u %s", arg_flags, arg_installation); + + system = dir_get_system (arg_installation, get_sender_pid (invocation), &error); + if (system == NULL) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + + if ((arg_flags & ~FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_ALL) != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_ALL)); + return TRUE; + } + + if (!check_for_system_helper_user (&passwd, &passwd_buf, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + + if (!get_connection_uid (invocation, &uid, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + + flatpak_dir = g_file_get_path (flatpak_dir_get_path (system)); + repo_tmp = g_build_filename (flatpak_dir, "repo", "tmp", NULL); + + if (reuse_cache_dir_if_available (repo_tmp, &src_dir, &passwd)) + g_debug ("Cache dir %s can be reused", src_dir); + else + { + /* Create a new cache dir and add it to cache_dirs_in_use. Do all this under + * a lock, so that a different pull does not snatch this directory up using + * reuse_cache_dir_if_available. */ + G_LOCK (cache_dirs_in_use); + src_dir = g_mkdtemp_full (g_build_filename (repo_tmp, "flatpak-cache-XXXXXX", NULL), 0755); + if (src_dir == NULL) + { + G_UNLOCK (cache_dirs_in_use); + glnx_throw_errno_prefix (&error, "Failed to create new cache-dir at %s", repo_tmp); + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + g_hash_table_insert (cache_dirs_in_use, g_strdup (src_dir), NULL); + G_UNLOCK (cache_dirs_in_use); + + if (chown (src_dir, passwd.pw_uid, passwd.pw_gid) == -1) + { + remove_dir_from_cache_dirs_in_use (src_dir); + glnx_throw_errno_prefix (&error, "Failed to chown %s to user %s", + src_dir, passwd.pw_name); + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + } + + new_pull = ongoing_pull_new (object, invocation, &passwd, uid, src_dir, &error); + if (error != NULL) + { + remove_dir_from_cache_dirs_in_use (src_dir); + g_dbus_method_invocation_return_gerror (invocation, error); + return TRUE; + } + + G_LOCK (cache_dirs_in_use); + g_hash_table_insert (cache_dirs_in_use, g_strdup (src_dir), new_pull); + G_UNLOCK (cache_dirs_in_use); + + fd_list = g_unix_fd_list_new (); + fd_index = g_unix_fd_list_append (fd_list, new_pull->client_socket, NULL); + + flatpak_system_helper_complete_get_revokefs_fd (object, invocation, + fd_list, g_variant_new_handle (fd_index), + new_pull->src_dir); + + return TRUE; +} + static gboolean handle_update_summary (FlatpakSystemHelper *object, GDBusMethodInvocation *invocation, @@ -1438,7 +1938,8 @@ flatpak_authorize_method_handler (GDBusInterfaceSkeleton *interface, else if (g_strcmp0 (method_name, "RemoveLocalRef") == 0 || g_strcmp0 (method_name, "PruneLocalRepo") == 0 || g_strcmp0 (method_name, "EnsureRepo") == 0 || - g_strcmp0 (method_name, "RunTriggers") == 0) + g_strcmp0 (method_name, "RunTriggers") == 0 || + g_strcmp0 (method_name, "GetRevokefsFd") == 0) { guint32 flags; @@ -1524,6 +2025,7 @@ on_bus_acquired (GDBusConnection *connection, g_signal_connect (helper, "handle-run-triggers", G_CALLBACK (handle_run_triggers), NULL); g_signal_connect (helper, "handle-update-summary", G_CALLBACK (handle_update_summary), NULL); g_signal_connect (helper, "handle-generate-oci-summary", G_CALLBACK (handle_generate_oci_summary), NULL); + g_signal_connect (helper, "handle-get-revokefs-fd", G_CALLBACK (handle_get_revokefs_fd), NULL); g_signal_connect (helper, "g-authorize-method", G_CALLBACK (flatpak_authorize_method_handler), @@ -1688,11 +2190,17 @@ main (int argc, NULL, NULL); + cache_dirs_in_use = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + /* Ensure we don't idle exit */ schedule_idle_callback (); main_loop = g_main_loop_new (NULL, FALSE); g_main_loop_run (main_loop); + G_LOCK (cache_dirs_in_use); + g_clear_pointer (&cache_dirs_in_use, g_hash_table_destroy); + G_UNLOCK (cache_dirs_in_use); + return 0; }