diff options
Diffstat (limited to '')
-rw-r--r-- | common/gdm-common.c | 976 |
1 files changed, 976 insertions, 0 deletions
diff --git a/common/gdm-common.c b/common/gdm-common.c new file mode 100644 index 0000000..b8de755 --- /dev/null +++ b/common/gdm-common.c @@ -0,0 +1,976 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "config.h" + +#include <string.h> +#include <unistd.h> +#include <stdlib.h> +#include <locale.h> +#include <fcntl.h> +#include <sys/wait.h> +#include <grp.h> +#include <pwd.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gio/gio.h> + +#include "gdm-common.h" + +#include <systemd/sd-login.h> + +#define GDM_DBUS_NAME "org.gnome.DisplayManager" +#define GDM_DBUS_LOCAL_DISPLAY_FACTORY_PATH "/org/gnome/DisplayManager/LocalDisplayFactory" +#define GDM_DBUS_LOCAL_DISPLAY_FACTORY_INTERFACE "org.gnome.DisplayManager.LocalDisplayFactory" + +G_DEFINE_QUARK (gdm-common-error, gdm_common_error); + +gboolean +gdm_clear_close_on_exec_flag (int fd) +{ + int flags; + + if (fd < 0) { + return FALSE; + } + + flags = fcntl (fd, F_GETFD, 0); + + if (flags < 0) { + return FALSE; + } + + if ((flags & FD_CLOEXEC) != 0) { + int status; + + status = fcntl (fd, F_SETFD, flags & ~FD_CLOEXEC); + + return status != -1; + } + + return TRUE; +} + +gboolean +gdm_get_pwent_for_name (const char *name, + struct passwd **pwentp) +{ + struct passwd *pwent; + + do { + errno = 0; + pwent = getpwnam (name); + } while (pwent == NULL && errno == EINTR); + + if (pwentp != NULL) { + *pwentp = pwent; + } + + return (pwent != NULL); +} + +static gboolean +gdm_get_grent_for_gid (gint gid, + struct group **grentp) +{ + struct group *grent; + + do { + errno = 0; + grent = getgrgid (gid); + } while (grent == NULL && errno == EINTR); + + if (grentp != NULL) { + *grentp = grent; + } + + return (grent != NULL); +} + +int +gdm_wait_on_and_disown_pid (int pid, + int timeout) +{ + int status; + int ret; + int num_tries; + int flags; + gboolean already_reaped; + + if (timeout > 0) { + flags = WNOHANG; + num_tries = 10 * timeout; + } else { + flags = 0; + num_tries = 0; + } + wait_again: + errno = 0; + already_reaped = FALSE; + ret = waitpid (pid, &status, flags); + if (ret < 0) { + if (errno == EINTR) { + goto wait_again; + } else if (errno == ECHILD) { + already_reaped = TRUE; + } else { + g_debug ("GdmCommon: waitpid () should not fail"); + } + } else if (ret == 0) { + num_tries--; + + if (num_tries > 0) { + g_usleep (G_USEC_PER_SEC / 10); + } else { + char *path; + char *command; + + path = g_strdup_printf ("/proc/%ld/cmdline", (long) pid); + if (g_file_get_contents (path, &command, NULL, NULL)) {; + g_warning ("GdmCommon: process (pid:%d, command '%s') isn't dying after %d seconds, now ignoring it.", + (int) pid, command, timeout); + g_free (command); + } else { + g_warning ("GdmCommon: process (pid:%d) isn't dying after %d seconds, now ignoring it.", + (int) pid, timeout); + } + g_free (path); + + return 0; + } + goto wait_again; + } + + g_debug ("GdmCommon: process (pid:%d) done (%s:%d)", + (int) pid, + already_reaped? "reaped earlier" : + WIFEXITED (status) ? "status" + : WIFSIGNALED (status) ? "signal" + : "unknown", + already_reaped? 1 : + WIFEXITED (status) ? WEXITSTATUS (status) + : WIFSIGNALED (status) ? WTERMSIG (status) + : -1); + + return status; +} + +int +gdm_wait_on_pid (int pid) +{ + return gdm_wait_on_and_disown_pid (pid, 0); +} + +int +gdm_signal_pid (int pid, + int signal) +{ + int status = -1; + + /* perhaps block sigchld */ + g_debug ("GdmCommon: sending signal %d to process %d", signal, pid); + errno = 0; + status = kill (pid, signal); + + if (status < 0) { + if (errno == ESRCH) { + g_warning ("Child process %d was already dead.", + (int)pid); + } else { + g_warning ("Couldn't kill child process %d: %s", + pid, + g_strerror (errno)); + } + } + + /* perhaps unblock sigchld */ + + return status; +} + +static gboolean +_fd_is_character_device (int fd) +{ + struct stat file_info; + + if (fstat (fd, &file_info) < 0) { + return FALSE; + } + + return S_ISCHR (file_info.st_mode); +} + +static gboolean +_read_bytes (int fd, + char *bytes, + gsize number_of_bytes, + GError **error) +{ + size_t bytes_left_to_read; + size_t total_bytes_read = 0; + gboolean premature_eof; + + bytes_left_to_read = number_of_bytes; + premature_eof = FALSE; + do { + size_t bytes_read = 0; + + errno = 0; + bytes_read = read (fd, ((guchar *) bytes) + total_bytes_read, + bytes_left_to_read); + + if (bytes_read > 0) { + total_bytes_read += bytes_read; + bytes_left_to_read -= bytes_read; + } else if (bytes_read == 0) { + premature_eof = TRUE; + break; + } else if ((errno != EINTR)) { + break; + } + } while (bytes_left_to_read > 0); + + if (premature_eof) { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_FAILED, + "No data available"); + + return FALSE; + } else if (bytes_left_to_read > 0) { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + return FALSE; + } + + return TRUE; +} + +/** + * Pulls a requested number of bytes from /dev/urandom + * + * @param size number of bytes to pull + * @param error error if read fails + * @returns The requested number of random bytes or #NULL if fail + */ + +char * +gdm_generate_random_bytes (gsize size, + GError **error) +{ + int fd; + char *bytes; + GError *read_error; + + /* We don't use the g_rand_* glib apis because they don't document + * how much entropy they are seeded with, and it might be less + * than the passed in size. + */ + + errno = 0; + fd = open ("/dev/urandom", O_RDONLY); + + if (fd < 0) { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + close (fd); + return NULL; + } + + if (!_fd_is_character_device (fd)) { + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (ENODEV), + _("/dev/urandom is not a character device")); + close (fd); + return NULL; + } + + bytes = g_malloc (size); + read_error = NULL; + if (!_read_bytes (fd, bytes, size, &read_error)) { + g_propagate_error (error, read_error); + g_free (bytes); + close (fd); + return NULL; + } + + close (fd); + return bytes; +} +static gboolean +create_transient_display (GDBusConnection *connection, + GError **error) +{ + GError *local_error = NULL; + GVariant *reply; + const char *value; + + reply = g_dbus_connection_call_sync (connection, + GDM_DBUS_NAME, + GDM_DBUS_LOCAL_DISPLAY_FACTORY_PATH, + GDM_DBUS_LOCAL_DISPLAY_FACTORY_INTERFACE, + "CreateTransientDisplay", + NULL, /* parameters */ + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, &local_error); + if (reply == NULL) { + g_warning ("Unable to create transient display: %s", local_error->message); + g_propagate_error (error, local_error); + return FALSE; + } + + g_variant_get (reply, "(&o)", &value); + g_debug ("Started %s", value); + + g_variant_unref (reply); + return TRUE; +} + +gboolean +gdm_activate_session_by_id (GDBusConnection *connection, + const char *seat_id, + const char *session_id) +{ + GError *local_error = NULL; + GVariant *reply; + + reply = g_dbus_connection_call_sync (connection, + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "ActivateSessionOnSeat", + g_variant_new ("(ss)", session_id, seat_id), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, &local_error); + if (reply == NULL) { + g_warning ("Unable to activate session: %s", local_error->message); + g_error_free (local_error); + return FALSE; + } + + g_variant_unref (reply); + + return TRUE; +} + +gboolean +gdm_get_login_window_session_id (const char *seat_id, + char **session_id) +{ + gboolean ret; + int res, i; + char **sessions; + char *service_id; + char *service_class; + char *state; + + res = sd_seat_get_sessions (seat_id, &sessions, NULL, NULL); + if (res < 0) { + g_debug ("Failed to determine sessions: %s", strerror (-res)); + return FALSE; + } + + if (sessions == NULL || sessions[0] == NULL) { + *session_id = NULL; + ret = FALSE; + goto out; + } + + for (i = 0; sessions[i]; i ++) { + + res = sd_session_get_class (sessions[i], &service_class); + if (res < 0) { + if (res == -ENXIO) + continue; + + g_debug ("failed to determine class of session %s: %s", sessions[i], strerror (-res)); + ret = FALSE; + goto out; + } + + if (strcmp (service_class, "greeter") != 0) { + free (service_class); + continue; + } + + free (service_class); + + ret = sd_session_get_state (sessions[i], &state); + if (ret < 0) { + if (res == -ENXIO) + continue; + + g_debug ("failed to determine state of session %s: %s", sessions[i], strerror (-res)); + ret = FALSE; + goto out; + } + + if (g_strcmp0 (state, "closing") == 0) { + free (state); + continue; + } + free (state); + + res = sd_session_get_service (sessions[i], &service_id); + if (res < 0) { + if (res == -ENXIO) + continue; + + g_debug ("failed to determine service of session %s: %s", sessions[i], strerror (-res)); + ret = FALSE; + goto out; + } + + if (strcmp (service_id, "gdm-launch-environment") == 0) { + *session_id = g_strdup (sessions[i]); + ret = TRUE; + + free (service_id); + goto out; + } + + free (service_id); + } + + *session_id = NULL; + ret = FALSE; + +out: + if (sessions) { + for (i = 0; sessions[i]; i ++) { + free (sessions[i]); + } + + free (sessions); + } + + return ret; +} + +static gboolean +goto_login_session (GDBusConnection *connection, + GError **error) +{ + gboolean ret; + int res; + char *our_session; + char *session_id; + char *seat_id; + GError *local_error = NULL; + + ret = FALSE; + session_id = NULL; + seat_id = NULL; + + /* First look for any existing LoginWindow sessions on the seat. + If none are found, create a new one. */ + + /* Note that we mostly use free () here, instead of g_free () + * since the data allocated is from libsystemd-logind, which + * does not use GLib's g_malloc (). */ + + if (!gdm_find_display_session (0, getuid (), &our_session, &local_error)) { + g_propagate_prefixed_error (error, local_error, _("Could not identify the current session: ")); + + return FALSE; + } + + res = sd_session_get_seat (our_session, &seat_id); + free (our_session); + if (res < 0) { + g_debug ("failed to determine own seat: %s", strerror (-res)); + g_set_error (error, GDM_COMMON_ERROR, 0, _("Could not identify the current seat.")); + + return FALSE; + } + + res = sd_seat_can_multi_session (seat_id); + if (res < 0) { + free (seat_id); + + g_debug ("failed to determine whether seat can do multi session: %s", strerror (-res)); + g_set_error (error, GDM_COMMON_ERROR, 0, _("The system is unable to determine whether to switch to an existing login screen or start up a new login screen.")); + + return FALSE; + } + + if (res == 0) { + free (seat_id); + + g_set_error (error, GDM_COMMON_ERROR, 0, _("The system is unable to start up a new login screen.")); + + return FALSE; + } + + res = gdm_get_login_window_session_id (seat_id, &session_id); + if (res && session_id != NULL) { + res = gdm_activate_session_by_id (connection, seat_id, session_id); + + if (res) { + ret = TRUE; + } + } + + if (! ret && g_strcmp0 (seat_id, "seat0") == 0) { + res = create_transient_display (connection, error); + if (res) { + ret = TRUE; + } + } + + free (seat_id); + g_free (session_id); + + return ret; +} + +gboolean +gdm_goto_login_session (GError **error) +{ + GError *local_error; + GDBusConnection *connection; + + local_error = NULL; + connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &local_error); + if (connection == NULL) { + g_debug ("Failed to connect to the D-Bus daemon: %s", local_error->message); + g_propagate_error (error, local_error); + return FALSE; + } + + return goto_login_session (connection, error); +} + +static void +listify_hash (const char *key, + const char *value, + GPtrArray *env) +{ + char *str; + str = g_strdup_printf ("%s=%s", key, value); + g_debug ("Gdm: script environment: %s", str); + g_ptr_array_add (env, str); +} + +GPtrArray * +gdm_get_script_environment (const char *username, + const char *display_name, + const char *display_hostname, + const char *display_x11_authority_file) +{ + GPtrArray *env; + GHashTable *hash; + struct passwd *pwent; + + env = g_ptr_array_new (); + + /* create a hash table of current environment, then update keys has necessary */ + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + /* modify environment here */ + g_hash_table_insert (hash, g_strdup ("HOME"), g_strdup ("/")); + g_hash_table_insert (hash, g_strdup ("PWD"), g_strdup ("/")); + g_hash_table_insert (hash, g_strdup ("SHELL"), g_strdup ("/bin/sh")); + + if (username != NULL) { + g_hash_table_insert (hash, g_strdup ("LOGNAME"), + g_strdup (username)); + g_hash_table_insert (hash, g_strdup ("USER"), + g_strdup (username)); + g_hash_table_insert (hash, g_strdup ("USERNAME"), + g_strdup (username)); + + gdm_get_pwent_for_name (username, &pwent); + if (pwent != NULL) { + if (pwent->pw_dir != NULL && pwent->pw_dir[0] != '\0') { + g_hash_table_insert (hash, g_strdup ("HOME"), + g_strdup (pwent->pw_dir)); + g_hash_table_insert (hash, g_strdup ("PWD"), + g_strdup (pwent->pw_dir)); + } + + g_hash_table_insert (hash, g_strdup ("SHELL"), + g_strdup (pwent->pw_shell)); + + /* Also get group name and propagate down */ + struct group *grent; + + if (gdm_get_grent_for_gid (pwent->pw_gid, &grent)) { + g_hash_table_insert (hash, g_strdup ("GROUP"), g_strdup (grent->gr_name)); + } + } + } + + if (display_hostname) { + g_hash_table_insert (hash, g_strdup ("REMOTE_HOST"), g_strdup (display_hostname)); + } + + /* Runs as root */ + if (display_x11_authority_file) { + g_hash_table_insert (hash, g_strdup ("XAUTHORITY"), g_strdup (display_x11_authority_file)); + } + + if (display_name) { + g_hash_table_insert (hash, g_strdup ("DISPLAY"), g_strdup (display_name)); + } + g_hash_table_insert (hash, g_strdup ("PATH"), g_strdup (GDM_SESSION_DEFAULT_PATH)); + g_hash_table_insert (hash, g_strdup ("RUNNING_UNDER_GDM"), g_strdup ("true")); + + g_hash_table_remove (hash, "MAIL"); + + g_hash_table_foreach (hash, (GHFunc)listify_hash, env); + g_hash_table_destroy (hash); + + g_ptr_array_add (env, NULL); + + return env; +} + +gboolean +gdm_run_script (const char *dir, + const char *username, + const char *display_name, + const char *display_hostname, + const char *display_x11_authority_file) +{ + char *script; + char **argv; + gint status; + GError *error; + GPtrArray *env; + gboolean res; + gboolean ret; + + ret = FALSE; + + g_assert (dir != NULL); + g_assert (username != NULL); + + script = g_build_filename (dir, display_name, NULL); + g_debug ("Trying script %s", script); + if (! (g_file_test (script, G_FILE_TEST_IS_REGULAR) + && g_file_test (script, G_FILE_TEST_IS_EXECUTABLE))) { + g_debug ("script %s not found; skipping", script); + g_free (script); + script = NULL; + } + + if (script == NULL + && display_hostname != NULL + && display_hostname[0] != '\0') { + script = g_build_filename (dir, display_hostname, NULL); + g_debug ("Trying script %s", script); + if (! (g_file_test (script, G_FILE_TEST_IS_REGULAR) + && g_file_test (script, G_FILE_TEST_IS_EXECUTABLE))) { + g_debug ("script %s not found; skipping", script); + g_free (script); + script = NULL; + } + } + + if (script == NULL) { + script = g_build_filename (dir, "Default", NULL); + g_debug ("Trying script %s", script); + if (! (g_file_test (script, G_FILE_TEST_IS_REGULAR) + && g_file_test (script, G_FILE_TEST_IS_EXECUTABLE))) { + g_debug ("script %s not found; skipping", script); + g_free (script); + script = NULL; + } + } + + if (script == NULL) { + g_debug ("no script found"); + return TRUE; + } + + g_debug ("Running process: %s", script); + error = NULL; + if (! g_shell_parse_argv (script, NULL, &argv, &error)) { + g_warning ("Could not parse command: %s", error->message); + g_error_free (error); + goto out; + } + + env = gdm_get_script_environment (username, + display_name, + display_hostname, + display_x11_authority_file); + + res = g_spawn_sync (NULL, + argv, + (char **)env->pdata, + G_SPAWN_SEARCH_PATH, + NULL, + NULL, + NULL, + NULL, + &status, + &error); + + g_ptr_array_foreach (env, (GFunc)g_free, NULL); + g_ptr_array_free (env, TRUE); + g_strfreev (argv); + + if (! res) { + g_warning ("Unable to run script: %s", error->message); + g_error_free (error); + } + + if (WIFEXITED (status)) { + g_debug ("Process exit status: %d", WEXITSTATUS (status)); + ret = WEXITSTATUS (status) == 0; + } + + out: + g_free (script); + + return ret; +} + +gboolean +gdm_shell_var_is_valid_char (gchar c, gboolean first) +{ + return (!first && g_ascii_isdigit (c)) || + c == '_' || + g_ascii_isalpha (c); +} + +/* This expands a string somewhat similar to how a shell would do it + if it was enclosed inside double quotes. It handles variable + expansion like $FOO and ${FOO}, single-char escapes using \, and + non-escaped # at the begining of a word is taken as a comment and ignored */ +char * +gdm_shell_expand (const char *str, + GdmExpandVarFunc expand_var_func, + gpointer user_data) +{ + GString *s = g_string_new(""); + const gchar *p, *start; + gchar c; + gboolean at_new_word; + + p = str; + at_new_word = TRUE; + while (*p) { + c = *p; + if (c == '\\') { + p++; + c = *p; + if (c != '\0') { + p++; + switch (c) { + case '\\': + g_string_append_c (s, '\\'); + break; + case '$': + g_string_append_c (s, '$'); + break; + case '#': + g_string_append_c (s, '#'); + break; + default: + g_string_append_c (s, '\\'); + g_string_append_c (s, c); + break; + } + } + } else if (c == '#' && at_new_word) { + break; + } else if (c == '$') { + gboolean brackets = FALSE; + p++; + if (*p == '{') { + brackets = TRUE; + p++; + } + start = p; + while (*p != '\0' && + gdm_shell_var_is_valid_char (*p, p == start)) + p++; + if (p == start || (brackets && *p != '}')) { + /* Invalid variable, use as-is */ + g_string_append_c (s, '$'); + if (brackets) + g_string_append_c (s, '{'); + g_string_append_len (s, start, p - start); + } else { + gchar *expanded; + gchar *var = g_strndup (start, p - start); + if (brackets && *p == '}') + p++; + + expanded = expand_var_func (var, user_data); + if (expanded) + g_string_append (s, expanded); + g_free (var); + g_free (expanded); + } + } else { + p++; + g_string_append_c (s, c); + at_new_word = g_ascii_isspace (c); + } + } + return g_string_free (s, FALSE); +} + +static gboolean +_systemd_session_is_graphical (const char *session_id) +{ + const gchar * const graphical_session_types[] = { "wayland", "x11", "mir", NULL }; + int saved_errno; + g_autofree gchar *type = NULL; + + saved_errno = sd_session_get_type (session_id, &type); + if (saved_errno < 0) { + g_warning ("Couldn't get type for session '%s': %s", + session_id, + g_strerror (-saved_errno)); + return FALSE; + } + + if (!g_strv_contains (graphical_session_types, type)) { + g_debug ("Session '%s' is not a graphical session (type: '%s')", + session_id, + type); + return FALSE; + } + + return TRUE; +} + +static gboolean +_systemd_session_is_active (const char *session_id) +{ + const gchar * const active_states[] = { "active", "online", NULL }; + int saved_errno; + g_autofree gchar *state = NULL; + + /* + * display sessions can be 'closing' if they are logged out but some + * processes are lingering; we shouldn't consider these (this is + * checking for a race condition since we specified + * GDM_SYSTEMD_SESSION_REQUIRE_ONLINE) + */ + saved_errno = sd_session_get_state (session_id, &state); + if (saved_errno < 0) { + g_warning ("Couldn't get state for session '%s': %s", + session_id, + g_strerror (-saved_errno)); + return FALSE; + } + + if (!g_strv_contains (active_states, state)) { + g_debug ("Session '%s' is not active or online", session_id); + return FALSE; + } + + return TRUE; +} + +gboolean +gdm_find_display_session (GPid pid, + const uid_t uid, + char **out_session_id, + GError **error) +{ + char *local_session_id = NULL; + g_auto(GStrv) sessions = NULL; + int n_sessions; + int res; + + g_return_val_if_fail (out_session_id != NULL, FALSE); + + /* First try to look up the session using the pid. We need this + * at least for the greeter, because it currently runs multiple + * sessions under the same user. + * See also commit 2b52d8933c8ab38e7ee83318da2363d00d8c5581 + * which added an explicit dbus-run-session for all but seat0. + */ + res = sd_pid_get_session (pid, &local_session_id); + if (res >= 0) { + g_debug ("GdmCommon: Found session %s for PID %d, using", local_session_id, pid); + + *out_session_id = g_strdup (local_session_id); + free (local_session_id); + + return TRUE; + } else { + if (res != -ENODATA) + g_warning ("GdmCommon: Failed to retrieve session information for pid %d: %s", + pid, strerror (-res)); + } + + g_debug ("Finding a graphical session for user %d", uid); + + n_sessions = sd_uid_get_sessions (uid, + GDM_SYSTEMD_SESSION_REQUIRE_ONLINE, + &sessions); + + if (n_sessions < 0) { + g_set_error (error, + GDM_COMMON_ERROR, + 0, + "Failed to get sessions for user %d", + uid); + return FALSE; + } + + for (int i = 0; i < n_sessions; ++i) { + g_debug ("Considering session '%s'", sessions[i]); + + if (!_systemd_session_is_graphical (sessions[i])) + continue; + + if (!_systemd_session_is_active (sessions[i])) + continue; + + /* + * We get the sessions from newest to oldest, so take the last + * one we find that's good + */ + local_session_id = sessions[i]; + } + + if (local_session_id == NULL) { + g_set_error (error, + GDM_COMMON_ERROR, + 0, + "Could not find a graphical session for user %d", + uid); + return FALSE; + } + + *out_session_id = g_strdup (local_session_id); + + return TRUE; +} |