]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
homectl: implement fallback shell stub
authorLennart Poettering <lennart@poettering.net>
Fri, 13 May 2022 22:03:16 +0000 (00:03 +0200)
committerLennart Poettering <lennart@poettering.net>
Wed, 14 Feb 2024 14:05:18 +0000 (15:05 +0100)
src/home/homectl.c
src/home/meson.build

index 72a24e7156265058a95e6b436ae3521dba19c89c..34c62f1edb59c8b1321941ac4c7092d3c5c51182 100644 (file)
@@ -740,7 +740,6 @@ static int inspect_home(int argc, char *argv[], void *userdata) {
                         r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i);
                 } else
                         r = bus_call_method(bus, bus_mgr, "GetUserRecordByUID", &error, &reply, "u", (uint32_t) uid);
-
                 if (r < 0) {
                         log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
                         if (ret == 0)
@@ -4094,6 +4093,197 @@ static int redirect_bus_mgr(void) {
         return 0;
 }
 
+static bool is_fallback_shell(const char *p) {
+        const char *q;
+
+        if (!p)
+                return false;
+
+        if (p[0] == '-') {
+                /* Skip over login shell dash */
+                p++;
+
+                if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
+                        return true;
+        }
+
+        q = strrchr(p, '/'); /* Skip over path */
+        if (q)
+                p = q + 1;
+
+        return streq(p, "systemd-home-fallback-shell");
+}
+
+static int fallback_shell(int argc, char *argv[]) {
+        _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL;
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_strv_free_ char **l = NULL;
+        _cleanup_free_ char *argv0 = NULL;
+        const char *json, *hd, *shell;
+        int r, incomplete;
+
+        /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory
+         * wasn't activated yet, SSH will permit the access but the home directory isn't actually available
+         * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't
+         * run the PAM authentication stack (because it authenticates via its own key management, after
+         * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the
+         * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to
+         * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell
+         * listed in user records whose home directory is not activated yet with this pseudo-shell. Net
+         * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir
+         * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is
+         * complete the user record will look like any other. */
+
+        r = acquire_bus(&bus);
+        if (r < 0)
+                return r;
+
+        for (unsigned n_tries = 0;; n_tries++) {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+                _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+                if (n_tries >= 5)
+                        return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                               "Failed to activate home dir, even after %u tries.", n_tries);
+
+                /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */
+                r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */
+                if (r < 0)
+                        return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+
+                r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+                if (r < 0)
+                        return bus_log_parse_error(r);
+
+                r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse JSON identity: %m");
+
+                hr = user_record_new();
+                if (!hr)
+                        return log_oom();
+
+                r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+                if (r < 0)
+                        return r;
+
+                if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
+                        break;
+
+                if (!secret) {
+                        r = acquire_passed_secrets(hr->user_name, &secret);
+                        if (r < 0)
+                                return r;
+                }
+
+                for (;;) {
+                        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+                        r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced");
+                        if (r < 0)
+                                return bus_log_create_error(r);
+
+                        r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+                        if (r < 0)
+                                return bus_log_create_error(r);
+
+                        r = bus_message_append_secret(m, secret);
+                        if (r < 0)
+                                return bus_log_create_error(r);
+
+                        r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
+                        if (r < 0) {
+                                if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_REFERENCED))
+                                        return log_error_errno(r, "Called without reference on home taken, can't operate.");
+
+                                r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
+                                if (r < 0)
+                                        return r;
+
+                                sd_bus_error_free(&error);
+                        } else
+                                break;
+                }
+
+                /* Try again */
+                hr = user_record_unref(hr);
+        }
+
+        incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */
+        if (incomplete < 0 && incomplete != -ENXIO)
+                return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m");
+        if (incomplete > 0) {
+                /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind
+                 * start the user@.service instance for us. */
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+                r = sd_bus_call_method(
+                                bus,
+                                "org.freedesktop.login1",
+                                "/org/freedesktop/login1/session/self",
+                                "org.freedesktop.login1.Session",
+                                "SetClass",
+                                &error,
+                                /* ret_reply= */ NULL,
+                                "s",
+                                "user");
+                if (r < 0)
+                        return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r));
+
+                if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */
+                        return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m");
+
+                if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
+                        return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
+        }
+
+        /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection
+         * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */
+        bus = sd_bus_flush_close_unref(bus);
+
+        assert(!hr->use_fallback);
+        assert_se(shell = user_record_shell(hr));
+        assert_se(hd = user_record_home_directory(hr));
+
+        /* Extra protection: avoid loops */
+        if (is_fallback_shell(shell))
+                return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name);
+
+        if (chdir(hd) < 0)
+                return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd);
+
+        if (setenv("SHELL", shell, /* overwrite= */ true) < 0)
+                return log_error_errno(errno, "Failed to set $SHELL: %m");
+
+        if (setenv("HOME", hd, /* overwrite= */ true) < 0)
+                return log_error_errno(errno, "Failed to set $HOME: %m");
+
+        /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */
+        FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN")
+                if (unsetenv(ue) < 0)
+                        return log_error_errno(errno, "Failed to unset $%s: %m", ue);
+
+        r = path_extract_filename(shell, &argv0);
+        if (r < 0)
+                return log_error_errno(r, "Unable to extract file name from '%s': %m", shell);
+        if (r == O_DIRECTORY)
+                return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell);
+
+        /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */
+        if (!argv || isempty(argv[0]) || argv[0][0] == '-')
+                argv0[0] = '-';
+
+        l = strv_new(argv0);
+        if (!l)
+                return log_oom();
+
+        if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0)
+                return log_oom();
+
+        execv(shell, l);
+        return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
+}
+
 static int run(int argc, char *argv[]) {
         static const Verb verbs[] = {
                 { "help",           VERB_ANY, VERB_ANY, 0,            help                 },
@@ -4125,6 +4315,9 @@ static int run(int argc, char *argv[]) {
         if (r < 0)
                 return r;
 
+        if (is_fallback_shell(argv[0]))
+                return fallback_shell(argc, argv);
+
         r = parse_argv(argc, argv);
         if (r <= 0)
                 return r;
index 09831dee4529afa2678cbc22e99cece02b753c9b..c6fc5f3cea5c6c4bbb1a8080449b6a05cdbc7e4b 100644 (file)
@@ -137,4 +137,8 @@ if conf.get('ENABLE_HOMED') == 1
                 install_data('homed.conf',
                              install_dir : pkgconfigfiledir)
         endif
+
+        meson.add_install_script(sh, '-c',
+                                 ln_s.format(bindir / 'homectl',
+                                             bindir / 'systemd-home-fallback-shell'))
 endif