diff --git a/common/flatpak-utils.h b/common/flatpak-utils.h index d1a276455..60c47ff5a 100644 --- a/common/flatpak-utils.h +++ b/common/flatpak-utils.h @@ -30,6 +30,11 @@ #include "flatpak-dir.h" #include +typedef enum { + FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV = 1 << 0, +} FlatpakHostCommandFlags; + + gboolean flatpak_fail (GError **error, const char *format, ...); diff --git a/data/org.freedesktop.Flatpak.xml b/data/org.freedesktop.Flatpak.xml index 41b198a99..22ae06677 100644 --- a/data/org.freedesktop.Flatpak.xml +++ b/data/org.freedesktop.Flatpak.xml @@ -29,6 +29,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/session-helper/flatpak-session-helper.c b/session-helper/flatpak-session-helper.c index d9366f7de..5b59dc18f 100644 --- a/session-helper/flatpak-session-helper.c +++ b/session-helper/flatpak-session-helper.c @@ -23,12 +23,32 @@ #include #include #include +#include #include +#include #include "flatpak-dbus.h" #include "flatpak-utils.h" static char *monitor_dir; +static guint32 next_client_pid = 0; +static GHashTable *client_pid_data_hash = NULL; +static GDBusConnection *session_bus = NULL; + +typedef struct { + GPid pid; + guint32 client_pid; + char *client; + guint child_watch; +} PidData; + +static void +pid_data_free (PidData *data) +{ + g_free (data->client); + g_free (data); +} + static gboolean handle_request_monitor (FlatpakSessionHelper *object, GDBusMethodInvocation *invocation, @@ -40,6 +60,284 @@ handle_request_monitor (FlatpakSessionHelper *object, return TRUE; } +static void +child_watch_died (GPid pid, + gint status, + gpointer user_data) +{ + PidData *pid_data = user_data; + guint32 client_pid = pid_data->client_pid; + g_autoptr(GVariant) signal_variant = NULL; + + g_debug ("Client Pid %d (%d) died", pid_data->client_pid, pid); + + signal_variant = g_variant_ref_sink (g_variant_new ("(uu)", client_pid, status)); + g_dbus_connection_emit_signal (session_bus, + pid_data->client, + "/org/freedesktop/Flatpak/SessionHelper", + "org.freedesktop.Flatpak.SessionHelper", + "HostCommandExited", + signal_variant, + NULL); + + /* This frees the pid_data, so be careful */ + g_hash_table_remove (client_pid_data_hash, GUINT_TO_POINTER(client_pid)); +} + +typedef struct { + int from; + int to; + int final; +} FdMapEntry; + +typedef struct { + FdMapEntry *fd_map; + int fd_map_len; + gboolean set_tty; + int tty; +} ChildSetupData; + +static void +child_setup_func (gpointer user_data) +{ + ChildSetupData *data = (ChildSetupData *)user_data; + FdMapEntry *fd_map = data->fd_map; + sigset_t set; + int i; + + /* Unblock all signals */ + sigemptyset (&set); + if (pthread_sigmask (SIG_SETMASK, &set, NULL) == -1) + { + g_error ("Failed to unblock signals when starting child"); + return; + } + + /* Reset the handlers for all signals to their defaults. */ + for (i = 1; i < NSIG; i++) + { + if (i != SIGSTOP && i != SIGKILL) + signal (i, SIG_DFL); + } + + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].from != fd_map[i].to) + { + dup2 (fd_map[i].from, fd_map[i].to); + close (fd_map[i].from); + } + } + + /* Second pass in case we needed an inbetween fd value to avoid conflicts */ + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].to != fd_map[i].final) + { + dup2 (fd_map[i].to, fd_map[i].final); + close (fd_map[i].to); + } + } + + /* We become our own session and process group, because it never makes sense + to share the flatpak-session-helper dbus activated process group */ + setsid (); + setpgid (0, 0); + + if (data->set_tty) + ioctl (data->tty, TIOCSCTTY, 0); +} + + +static gboolean +handle_host_command (FlatpakSessionHelper *object, + GDBusMethodInvocation *invocation, + const gchar *arg_cwd_path, + const gchar *const *arg_argv, + GVariant *arg_fds, + GVariant *arg_envs, + guint flags) +{ + g_autoptr(GError) error = NULL; + GDBusMessage *message = g_dbus_method_invocation_get_message (invocation); + GUnixFDList *fd_list = g_dbus_message_get_unix_fd_list (message); + ChildSetupData child_setup_data = { NULL }; + GPid pid; + PidData *pid_data; + gsize i, j, n_fds, n_envs; + const gint *fds; + g_autofree FdMapEntry *fd_map = NULL; + gchar **env; + gint32 max_fd; + + if (*arg_cwd_path == 0) + arg_cwd_path = NULL; + + if (*arg_argv[0] == 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "No command given"); + return TRUE; + } + + + g_debug ("Running host command %s", arg_argv[0]); + + n_fds = 0; + fds = NULL; + if (fd_list != NULL) + { + n_fds = g_variant_n_children (arg_fds); + fds = g_unix_fd_list_peek_fds (fd_list, NULL); + } + fd_map = g_new0 (FdMapEntry, n_fds); + + child_setup_data.fd_map = fd_map; + child_setup_data.fd_map_len = n_fds; + + max_fd = -1; + for (i = 0; i < n_fds; i++) + { + gint32 handle, fd; + g_variant_get_child (arg_fds, i, "{uh}", &fd, &handle); + fd_map[i].to = fd; + fd_map[i].from = fds[i]; + fd_map[i].final = fd_map[i].to; + + /* If stdin/out/err is a tty we try to set it as the controlling + tty for the app, this way we can use this to run in a terminal. */ + if ((fd == 0 || fd == 1 || fd == 2) && + !child_setup_data.set_tty && + isatty (fds[i])) + { + child_setup_data.set_tty = TRUE; + child_setup_data.tty = fds[i]; + } + + max_fd = MAX (max_fd, fd_map[i].to); + max_fd = MAX (max_fd, fd_map[i].from); + } + + /* We make a second pass over the fds to find if any "to" fd index + overlaps an already in use fd (i.e. one in the "from" category + that are allocated randomly). If a fd overlaps "to" fd then its + a caller issue and not our fault, so we ignore that. */ + for (i = 0; i < n_fds; i++) + { + int to_fd = fd_map[i].to; + gboolean conflict = FALSE; + + /* At this point we're fine with using "from" values for this + value (because we handle to==from in the code), or values + that are before "i" in the fd_map (because those will be + closed at this point when dup:ing). However, we can't + reuse a fd that is in "from" for j > i. */ + for (j = i + 1; j < n_fds; j++) + { + int from_fd = fd_map[j].from; + if (from_fd == to_fd) + { + conflict = TRUE; + break; + } + } + + if (conflict) + fd_map[i].to = ++max_fd; + } + + if (flags & FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV) + { + char *empty[] = { NULL }; + env = g_strdupv (empty); + } + else + env = g_get_environ (); + + n_envs = g_variant_n_children (arg_envs); + for (i = 0; i < n_envs; i++) + { + const char *var = NULL; + const char *val = NULL; + g_variant_get_child (arg_envs, i, "{&s&s}", &var, &val); + + env = g_environ_setenv (env, var, val, TRUE); + } + + if (!g_spawn_async_with_pipes (arg_cwd_path, + (char **)arg_argv, + env, + G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD, + child_setup_func, &child_setup_data, + &pid, + NULL, + NULL, + NULL, + &error)) + { + gint code = G_DBUS_ERROR_FAILED; + if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_ACCES)) + code = G_DBUS_ERROR_ACCESS_DENIED; + else if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) + code = G_DBUS_ERROR_FILE_NOT_FOUND; + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, code, + "Failed to start command: %s", + error->message); + } + + pid_data = g_new0 (PidData, 1); + pid_data->pid = pid; + pid_data->client_pid = ++next_client_pid; + pid_data->client = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + pid_data->child_watch = g_child_watch_add_full (G_PRIORITY_DEFAULT, + pid, + child_watch_died, + pid_data, + NULL); + + g_debug ("Client Pid is %d (%d)", pid_data->client_pid, pid); + + g_hash_table_replace (client_pid_data_hash, GUINT_TO_POINTER(pid_data->client_pid), + pid_data); + + + flatpak_session_helper_complete_host_command (object, invocation, + pid_data->client_pid); + return TRUE; +} + +static gboolean +handle_host_command_signal (FlatpakSessionHelper *object, + GDBusMethodInvocation *invocation, + guint arg_pid, + guint arg_signal, + gboolean to_process_group) +{ + PidData *pid_data = NULL; + + pid_data = g_hash_table_lookup (client_pid_data_hash, GUINT_TO_POINTER(arg_pid)); + if (pid_data == NULL || + strcmp (pid_data->client, g_dbus_method_invocation_get_sender (invocation)) != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_UNIX_PROCESS_ID_UNKNOWN, + "No such pid"); + return TRUE; + } + + g_debug ("Sending signal %d to client pid %d", arg_signal, arg_pid); + + if (to_process_group) + killpg (pid_data->pid, arg_signal); + else + kill (pid_data->pid, arg_signal); + + flatpak_session_helper_complete_host_command_signal (object, invocation); + + return TRUE; +} + static void on_bus_acquired (GDBusConnection *connection, const gchar *name, @@ -51,6 +349,8 @@ on_bus_acquired (GDBusConnection *connection, helper = flatpak_session_helper_skeleton_new (); g_signal_connect (helper, "handle-request-monitor", G_CALLBACK (handle_request_monitor), NULL); + g_signal_connect (helper, "handle-host-command", G_CALLBACK (handle_host_command), NULL); + g_signal_connect (helper, "handle-host-command-signal", G_CALLBACK (handle_host_command_signal), NULL); if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (helper), connection, @@ -190,6 +490,15 @@ main (int argc, flatpak_migrate_from_xdg_app (); + client_pid_data_hash = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)pid_data_free); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (session_bus == NULL) + { + g_printerr ("Can't find bus: %s\n", error->message); + return 1; + } + monitor_dir = g_build_filename (g_get_user_runtime_dir (), "flatpak-monitor", NULL); if (g_mkdir_with_parents (monitor_dir, 0755) != 0) {