/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2007 William Jon McCann * * 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 #include #include #include #include #include #include #include #include #include #include #include #include "gdm-common.h" #include #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_literal (error, G_FILE_ERROR, g_file_error_from_errno (errno), 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_literal (error, G_FILE_ERROR, g_file_error_from_errno (errno), 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 = 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; } static void load_env_file (GFile *file, GdmLoadEnvVarFunc load_env_func, GdmExpandVarFunc expand_func, gpointer user_data) { gchar *contents; gchar **lines; gchar *line, *p; gchar *var, *var_end; gchar *expanded; char *filename; int i; filename = g_file_get_path (file); g_debug ("Loading env vars from %s\n", filename); g_free (filename); if (g_file_load_contents (file, NULL, &contents, NULL, NULL, NULL)) { lines = g_strsplit (contents, "\n", -1); g_free (contents); for (i = 0; lines[i] != NULL; i++) { line = lines[i]; p = line; while (g_ascii_isspace (*p)) p++; if (*p == '#' || *p == '\0') continue; var = p; while (gdm_shell_var_is_valid_char (*p, p == var)) p++; var_end = p; while (g_ascii_isspace (*p)) p++; if (var == var_end || *p != '=') { g_warning ("Invalid env.d line '%s'\n", line); continue; } *var_end = 0; p++; /* Skip = */ while (g_ascii_isspace (*p)) p++; expanded = gdm_shell_expand (p, expand_func, user_data); expanded = g_strchomp (expanded); load_env_func (var, expanded, user_data); g_free (expanded); } g_strfreev (lines); } } static gint compare_str (gconstpointer a, gconstpointer b) { return strcmp (*(const char **)a, *(const char **)b); } static void gdm_load_env_dir (GFile *dir, GdmLoadEnvVarFunc load_env_func, GdmExpandVarFunc expand_func, gpointer user_data) { GFileInfo *info = NULL; GFileEnumerator *enumerator = NULL; GPtrArray *names = NULL; GFile *file; const gchar *name; int i; enumerator = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_STANDARD_NAME"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN"," G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (!enumerator) { goto out; } names = g_ptr_array_new_with_free_func (g_free); while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL)) != NULL) { if (g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR && !g_file_info_get_is_hidden (info) && g_str_has_suffix (g_file_info_get_name (info), ".env")) g_ptr_array_add (names, g_strdup (g_file_info_get_name (info))); g_clear_object (&info); } g_ptr_array_sort (names, compare_str); for (i = 0; i < names->len; i++) { name = g_ptr_array_index (names, i); file = g_file_get_child (dir, name); load_env_file (file, load_env_func, expand_func, user_data); g_object_unref (file); } out: g_clear_pointer (&names, g_ptr_array_unref); g_clear_object (&enumerator); } void gdm_load_env_d (GdmLoadEnvVarFunc load_env_func, GdmExpandVarFunc expand_func, gpointer user_data) { GFile *dir; dir = g_file_new_for_path (DATADIR "/gdm/env.d"); gdm_load_env_dir (dir, load_env_func, expand_func, user_data); g_object_unref (dir); dir = g_file_new_for_path (GDMCONFDIR "/env.d"); gdm_load_env_dir (dir, load_env_func, expand_func, user_data); g_object_unref (dir); }