flatpak/session-helper/flatpak-session-helper.c
Christian Hergert b3489c3557 session-helper: use proper fd with ioctl() for tty setup
This was using the "from" fd from the fd_map, which will have already been
closed by time we reach this portion of the child setup.

Tracking the movement of FDs while resolving the remappings is rather
tedious and error prone, so just locate the final fd before calling
the ioctl() in child setup.
2016-09-15 21:38:06 -07:00

553 lines
16 KiB
C

/*
* Copyright © 2014 Red Hat, Inc
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors:
* Alexander Larsson <alexl@redhat.com>
*/
#include "config.h"
#include <locale.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include "flatpak-dbus.h"
#include "flatpak-utils.h"
static char *monitor_dir;
static GHashTable *client_pid_data_hash = NULL;
static GDBusConnection *session_bus = NULL;
typedef struct {
GPid 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,
gpointer user_data)
{
flatpak_session_helper_complete_request_monitor (object, invocation,
monitor_dir);
return TRUE;
}
static void
child_watch_died (GPid pid,
gint status,
gpointer user_data)
{
PidData *pid_data = user_data;
g_autoptr(GVariant) signal_variant = NULL;
g_debug ("Client Pid %d died", pid_data->pid);
signal_variant = g_variant_ref_sink (g_variant_new ("(uu)", pid, status));
g_dbus_connection_emit_signal (session_bus,
pid_data->client,
"/org/freedesktop/Flatpak/Development",
"org.freedesktop.Flatpak.Development",
"HostCommandExited",
signal_variant,
NULL);
/* This frees the pid_data, so be careful */
g_hash_table_remove (client_pid_data_hash, GUINT_TO_POINTER(pid_data->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)
{
/* data->tty is our from fd which is closed at this point.
* so locate the destnation fd and use it for the ioctl.
*/
for (i = 0; i < data->fd_map_len; i++)
{
if (fd_map[i].from == data->tty)
{
if (ioctl (fd_map[i].final, TIOCSCTTY, 0) == -1)
g_warning ("ioctl(%d, TIOCSCTTY, 0) failed: %s",
fd_map[i].final, strerror (errno));
break;
}
}
}
}
static gboolean
handle_host_command (FlatpakDevelopment *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 == NULL || *arg_argv == NULL || *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);
return TRUE;
}
pid_data = g_new0 (PidData, 1);
pid_data->pid = 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", pid_data->pid);
g_hash_table_replace (client_pid_data_hash, GUINT_TO_POINTER(pid_data->pid),
pid_data);
flatpak_development_complete_host_command (object, invocation,
pid_data->pid);
return TRUE;
}
static gboolean
handle_host_command_signal (FlatpakDevelopment *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_development_complete_host_command_signal (object, invocation);
return TRUE;
}
static void
on_bus_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
FlatpakSessionHelper *helper;
FlatpakDevelopment *devel;
GError *error = NULL;
helper = flatpak_session_helper_skeleton_new ();
g_signal_connect (helper, "handle-request-monitor", G_CALLBACK (handle_request_monitor), NULL);
if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (helper),
connection,
"/org/freedesktop/Flatpak/SessionHelper",
&error))
{
g_warning ("error: %s\n", error->message);
g_error_free (error);
}
devel = flatpak_development_skeleton_new ();
g_signal_connect (devel, "handle-host-command", G_CALLBACK (handle_host_command), NULL);
g_signal_connect (devel, "handle-host-command-signal", G_CALLBACK (handle_host_command_signal), NULL);
if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (devel),
connection,
"/org/freedesktop/Flatpak/Development",
&error))
{
g_warning ("error: %s\n", error->message);
g_error_free (error);
}
}
static void
on_name_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
}
static void
on_name_lost (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
exit (1);
}
static void
copy_file (const char *source,
const char *target_dir)
{
char *basename = g_path_get_basename (source);
char *dest = g_build_filename (target_dir, basename, NULL);
gchar *contents = NULL;
gsize len;
if (g_file_get_contents (source, &contents, &len, NULL))
g_file_set_contents (dest, contents, len, NULL);
g_free (basename);
g_free (dest);
g_free (contents);
}
static void
file_changed (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
char *source)
{
if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ||
event_type == G_FILE_MONITOR_EVENT_CREATED)
copy_file (source, monitor_dir);
}
static void
setup_file_monitor (const char *source)
{
GFile *s = g_file_new_for_path (source);
GFileMonitor *monitor;
copy_file (source, monitor_dir);
monitor = g_file_monitor_file (s, G_FILE_MONITOR_NONE, NULL, NULL);
if (monitor)
g_signal_connect (monitor, "changed", G_CALLBACK (file_changed), (char *) source);
}
static void
message_handler (const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer user_data)
{
/* Make this look like normal console output */
if (log_level & G_LOG_LEVEL_DEBUG)
g_printerr ("XA: %s\n", message);
else
g_printerr ("%s: %s\n", g_get_prgname (), message);
}
int
main (int argc,
char **argv)
{
guint owner_id;
GMainLoop *loop;
gboolean replace;
gboolean verbose;
gboolean show_version;
GOptionContext *context;
GBusNameOwnerFlags flags;
g_autoptr(GError) error = NULL;
const GOptionEntry options[] = {
{ "replace", 'r', 0, G_OPTION_ARG_NONE, &replace, "Replace old daemon.", NULL },
{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Enable debug output.", NULL },
{ "version", 0, 0, G_OPTION_ARG_NONE, &show_version, "Show program version.", NULL},
{ NULL }
};
setlocale (LC_ALL, "");
g_setenv ("GIO_USE_VFS", "local", TRUE);
g_set_prgname (argv[0]);
g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, message_handler, NULL);
context = g_option_context_new ("");
g_option_context_set_summary (context, "Flatpak session helper");
g_option_context_add_main_entries (context, options, GETTEXT_PACKAGE);
replace = FALSE;
verbose = FALSE;
show_version = FALSE;
if (!g_option_context_parse (context, &argc, &argv, &error))
{
g_printerr ("%s: %s", g_get_application_name(), error->message);
g_printerr ("\n");
g_printerr ("Try \"%s --help\" for more information.",
g_get_prgname ());
g_printerr ("\n");
g_option_context_free (context);
return 1;
}
if (show_version)
{
g_print (PACKAGE_STRING "\n");
return 0;
}
if (verbose)
g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL);
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)
{
g_print ("Can't create %s\n", monitor_dir);
exit (1);
}
setup_file_monitor ("/etc/resolv.conf");
setup_file_monitor ("/etc/localtime");
flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
if (replace)
flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;
owner_id = g_bus_own_name (G_BUS_TYPE_SESSION,
"org.freedesktop.Flatpak",
flags,
on_bus_acquired,
on_name_acquired,
on_name_lost,
NULL,
NULL);
loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (loop);
g_bus_unown_name (owner_id);
return 0;
}