From c63c6413e79ae969c67e138e33a73699796f68ce Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Feb 2025 22:43:41 +0100 Subject: [PATCH 1/8] fs-util: add some assert()s --- src/basic/fs-util.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 3b3fa1811e..88e37cd6a2 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -77,6 +77,11 @@ int rmdir_parents(const char *path, const char *stop) { int rename_noreplace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) { int r; + assert(olddirfd >= 0 || olddirfd == AT_FDCWD); + assert(oldpath); + assert(newdirfd >= 0 || newdirfd == AT_FDCWD); + assert(newpath); + /* Try the ideal approach first */ if (renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE) >= 0) return 0; From 5856e869bbf6a9522b4cc67a326d108f2950174b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Feb 2025 15:17:22 +0100 Subject: [PATCH 2/8] pam_systemd_home: update comment Follow-up for 563c5511ad0dd8763eaff75db1967249f662f844, which turned boolean parameters into flags. --- src/home/pam_systemd_home.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 9e54971d23..dcb85adc61 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -553,24 +553,24 @@ static int acquire_home( /* This acquires a reference to a home directory in the following ways: * - * 1. If please_authenticate is false, it tries to call RefHome() first — which - * will get us a reference to the home without authentication (which will work for homes that are - * not encrypted, or that already are activated). If this works, we are done. Yay! + * 1. If ACQUIRE_MUST_AUTHENTICATE is not set, it tries to call RefHome() first — which will get us a + * reference to the home without authentication (which will work for homes that are not encrypted, + * or that already are activated). If this works, we are done. Yay! * * 2. Otherwise, we'll call AcquireHome() — which will try to activate the home getting us a * reference. If this works, we are done. Yay! * - * 3. if ref_anyway, we'll call RefHomeUnrestricted() — which will give us a reference in any case - * (even if the activation failed!). + * 3. if ACQUIRE_REF_ANYWAY is set, we'll call RefHomeUnrestricted() — which will give us a reference + * in any case (even if the activation failed!). * - * The idea is that please_authenticate is set to false for the PAM session hooks (since for those - * authentication doesn't matter), and true for the PAM authentication hooks (since for those - * authentication is essential). And ref_anyway should be set if we are pretty sure that we can later - * activate the home directory via our fallback shell logic, and hence are OK if we can't activate - * things here. Usecase for that are SSH logins where SSH does the authentication and thus only the - * session hooks are called. But from the session hooks SSH doesn't allow asking questions, hence we - * simply allow the login attempt to continue but then invoke our fallback shell that will prompt the - * user for the missing unlock credentials, and then chainload the real shell. + * The idea is that ACQUIRE_MUST_AUTHENTICATE is off for the PAM session hooks (since for those + * authentication doesn't matter), and on for the PAM authentication hooks (since for those + * authentication is essential). And ACQUIRE_REF_ANYWAY should be set if we are pretty sure that we + * can later activate the home directory via our fallback shell logic, and hence are OK if we can't + * activate things here. Usecase for that are SSH logins where SSH does the authentication and thus + * only the session hooks are called. But from the session hooks SSH doesn't allow asking questions, + * hence we simply allow the login attempt to continue but then invoke our fallback shell that will + * prompt the user for the missing unlock credentials, and then chainload the real shell. */ r = pam_get_user(handle, &username, NULL); From 9a4f9e84c4ba79f39c7f5ab4b4d1a099a7496d52 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Feb 2025 15:13:08 +0100 Subject: [PATCH 3/8] pam_systemd_home: tweak order in authentication stack Let's move pam_systemd_home before pam_unix in the authentication hook. Since a while we are exposing shadow entries for homed log entries via NSS. This means that pam_unix now potentially has enough data for authenticating a user on its own, without letting pam_systemd_home do that. This is superficially OK, but also means that authentication will always go via password, even if pkcs11/fido2 is registered. Let's move this around, but be careful about it: let's list the precise errors which we think are enough to terminating further PAM processing, so that pam_unix comes into control in all cases where it's not clear that pam_systemd_home owns the user record. This previously wasn't visible to me, because on Fedora until authselect 1.5.1 (released earleir this year) the NSS shadow stuff was not enabled. This does the same also for the "account" stack, except that the order there already was as we want it. Finally, shorten the account stack, by just requiring pam_unix.so and dropping pam_permit.so, because it doesn't really serve much purpose (and Fedora doesn't use it by default either.) --- factory/etc/pam.d/system-auth | 7 +++---- man/pam_systemd.xml | 7 +++---- man/pam_systemd_home.xml | 7 +++---- src/login/systemd-user.in | 5 ++--- src/run/systemd-run0.in | 2 +- test/units/TEST-46-HOMED.sh | 7 +++---- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/factory/etc/pam.d/system-auth b/factory/etc/pam.d/system-auth index cb4e570361..60e657eb4b 100644 --- a/factory/etc/pam.d/system-auth +++ b/factory/etc/pam.d/system-auth @@ -3,14 +3,13 @@ # You really want to adjust this to your local distribution. If you use this # unmodified you are not building systems safely and securely. +-auth [success=done authtok_err=bad perm_denied=bad maxtries=bad default=ignore] pam_systemd_home.so auth sufficient pam_unix.so --auth sufficient pam_systemd_home.so auth required pam_deny.so account required pam_nologin.so --account sufficient pam_systemd_home.so -account sufficient pam_unix.so -account required pam_permit.so +-account [success=done authtok_expired=bad new_authtok_reqd=bad maxtries=bad acct_expired=bad default=ignore] pam_systemd_home.so +account required pam_unix.so -password sufficient pam_systemd_home.so password sufficient pam_unix.so sha512 shadow try_first_pass diff --git a/man/pam_systemd.xml b/man/pam_systemd.xml index f240cc755a..c488e5bec4 100644 --- a/man/pam_systemd.xml +++ b/man/pam_systemd.xml @@ -476,14 +476,13 @@ pam_set_data(handle, "systemd.runtime_max_sec", (void *)"3600", cleanup); systemd-logind.service: #%PAM-1.0 +-auth [success=done authtok_err=bad perm_denied=bad maxtries=bad default=ignore] pam_systemd_home.so auth sufficient pam_unix.so --auth sufficient pam_systemd_home.so auth required pam_deny.so account required pam_nologin.so --account sufficient pam_systemd_home.so -account sufficient pam_unix.so -account required pam_permit.so +-account [success=done authtok_expired=bad new_authtok_reqd=bad maxtries=bad acct_expired=bad default=ignore] pam_systemd_home.so +account required pam_unix.so -password sufficient pam_systemd_home.so password sufficient pam_unix.so sha512 shadow try_first_pass diff --git a/man/pam_systemd_home.xml b/man/pam_systemd_home.xml index ed117cfed2..066a038655 100644 --- a/man/pam_systemd_home.xml +++ b/man/pam_systemd_home.xml @@ -195,14 +195,13 @@ lennart@zeta$ cp -av /etc/skel ~/Areas/versuch1 systemd-homed.service to log in: #%PAM-1.0 + -auth [success=done authtok_err=bad perm_denied=bad maxtries=bad default=ignore] pam_systemd_home.so auth sufficient pam_unix.so --auth sufficient pam_systemd_home.so auth required pam_deny.so account required pam_nologin.so --account sufficient pam_systemd_home.so -account sufficient pam_unix.so -account required pam_permit.so +-account [success=done authtok_expired=bad new_authtok_reqd=bad maxtries=bad acct_expired=bad default=ignore] pam_systemd_home.so +account required pam_unix.so -password sufficient pam_systemd_home.so password sufficient pam_unix.so sha512 shadow try_first_pass diff --git a/src/login/systemd-user.in b/src/login/systemd-user.in index 8a3c9e0165..ce22744501 100644 --- a/src/login/systemd-user.in +++ b/src/login/systemd-user.in @@ -4,10 +4,9 @@ # Used by systemd --user instances. {% if ENABLE_HOMED %} --account sufficient pam_systemd_home.so +-account [success=done authtok_expired=bad new_authtok_reqd=bad maxtries=bad acct_expired=bad default=ignore] pam_systemd_home.so {% endif %} -account sufficient pam_unix.so no_pass_expiry -account required pam_permit.so +account required pam_unix.so no_pass_expiry {% if HAVE_SELINUX %} session required pam_selinux.so close diff --git a/src/run/systemd-run0.in b/src/run/systemd-run0.in index 11f830b7b0..c628ef237d 100644 --- a/src/run/systemd-run0.in +++ b/src/run/systemd-run0.in @@ -4,7 +4,7 @@ # Used by run0 sessions {% if ENABLE_HOMED %} --account sufficient pam_systemd_home.so +-account [success=done authtok_expired=bad new_authtok_reqd=bad maxtries=bad acct_expired=bad default=ignore] pam_systemd_home.so {% endif %} account required pam_unix.so diff --git a/test/units/TEST-46-HOMED.sh b/test/units/TEST-46-HOMED.sh index 5d9799211e..78bfefa48d 100755 --- a/test/units/TEST-46-HOMED.sh +++ b/test/units/TEST-46-HOMED.sh @@ -576,12 +576,11 @@ if command -v ssh &>/dev/null && command -v sshd &>/dev/null && ! [[ -v ASAN_OPT if [[ -f "$dir/pam.d/sshd" ]]; then mv "$dir/pam.d/sshd" "$dir/pam.d/sshd.bak" cat >"$dir/pam.d/sshd" < Date: Wed, 26 Feb 2025 18:13:10 +0100 Subject: [PATCH 4/8] pam-systemd: modernize export_legacy_dbus_address() a bit Let's log about unexpected access() failures among other things --- src/login/pam_systemd.c | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index eca3283da8..280e0a506d 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -370,14 +370,14 @@ static int export_legacy_dbus_address( pam_handle_t *handle, const char *runtime) { - const char *s; - _cleanup_free_ char *t = NULL; - int r = PAM_BUF_ERR; + int r; + + assert(handle); /* We need to export $DBUS_SESSION_BUS_ADDRESS because various applications will not connect - * correctly to the bus without it. This setting matches what dbus.socket does for the user - * session using 'systemctl --user set-environment'. We want to have the same configuration - * in processes started from the PAM session. + * correctly to the bus without it. This setting matches what dbus.socket does for the user session + * using 'systemctl --user set-environment'. We want to have the same configuration in processes + * started from the PAM session. * * The setting of the address is guarded by the access() check because it is also possible to compile * dbus without --enable-user-session, in which case this socket is not used, and @@ -386,14 +386,22 @@ static int export_legacy_dbus_address( * expect the socket to be present by the time we do this check, so we can just as well check once * here. */ - s = strjoina(runtime, "/bus"); - if (access(s, F_OK) < 0) + if (!runtime) return PAM_SUCCESS; + const char *s = strjoina(runtime, "/bus"); + if (access(s, F_OK) < 0) { + if (errno != ENOENT) + pam_syslog_errno(handle, LOG_WARNING, errno, "Failed to check if %s/bus exists, ignoring: %m", runtime); + + return PAM_SUCCESS; + } + + _cleanup_free_ char *t = NULL; if (asprintf(&t, DEFAULT_USER_BUS_ADDRESS_FMT, runtime) < 0) return pam_log_oom(handle); - r = pam_misc_setenv(handle, "DBUS_SESSION_BUS_ADDRESS", t, 0); + r = pam_misc_setenv(handle, "DBUS_SESSION_BUS_ADDRESS", t, /* readonly= */ false); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set bus variable: @PAMERR@"); From e35b78f14f15815011becd98fe9a844a4b5d02f8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 26 Feb 2025 18:14:06 +0100 Subject: [PATCH 5/8] pam-systemd: rework update_environment() Let's tweak update_environment() a bit: instead of being a NOP when no value is specified, let's actively unset the specified environment variable if it is set. This shouldn't change much, since for the cases we call the function so far the env vars in question should not be set before us in a way we'd set them differently. However, this is nice preparation for later, as we can make use of this for XDG_AREA which we might want to unset if we consider the area dir invalid. --- src/login/pam_systemd.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 280e0a506d..716036058a 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -585,12 +585,26 @@ static int update_environment(pam_handle_t *handle, const char *key, const char assert(handle); assert(key); - /* Updates the environment, but only if there's actually a value set. Also, log about errors */ + /* Updates the environment, and removes environment variables if value is NULL or empty. Also, log + * about errors. */ + + if (isempty(value)) { + /* Unset the variable if set. Note that pam_putenv() would log nastily behind our back if we + * call it without the variable actually being set. Hence we check explicitly if it's set + * before. */ + + if (!pam_getenv(handle, key)) + return PAM_SUCCESS; + + r = pam_putenv(handle, key); + if (!IN_SET(r, PAM_SUCCESS, PAM_BAD_ITEM)) + return pam_syslog_pam_error(handle, LOG_WARNING, r, + "Failed to unset %s environment variable: @PAMERR@", key); - if (isempty(value)) return PAM_SUCCESS; + } - r = pam_misc_setenv(handle, key, value, 0); + r = pam_misc_setenv(handle, key, value, /* readonly= */ false); if (r != PAM_SUCCESS) return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set environment variable %s: @PAMERR@", key); From cfb7abc7fc8a7a3a79d44d0511e65a40566f1949 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 23 Feb 2025 03:12:16 +0100 Subject: [PATCH 6/8] pam_systemd: complement per-area $HOME management with per-area $XDG_RUNTIME_DIRECTORY mgmt When a user logs into a non-default area we give them a private $HOME for that area (that's what 'area' is supposed to be after all). We so far left $XDG_RUNTIME_DIRECTORY as it was. Let's change that and mirror the subdirectory logic there too. Why? $XDG_RUNTIME_DIR is generally the place where AF_UNIX sockets are bound that can be used to connect to per-user services. (in particular all those which are behind D-Bus.) If we don't patch $XDG_RUNTIME_DIR like this then this means all the backing services will use the main area, which is problematic (since clients and services will disagree on $HOME), and makes it impossible to support the area concept for graphical logins properly. This does not actually make graphical logins work, but it at least makes them fail cleanly. That's because this patch alone won't make sure a per-area service manager/dbus instance is invoked automatically. That however can be added later, in a patch to logind. --- src/login/pam_systemd.c | 213 +++++++++++++++++++++++++++++----------- 1 file changed, 153 insertions(+), 60 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 716036058a..6dbf22d7b3 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -54,6 +54,7 @@ #include "stdio-util.h" #include "strv.h" #include "terminal-util.h" +#include "tmpfile-util.h" #include "user-util.h" #include "userdb.h" @@ -832,27 +833,6 @@ static int apply_user_record_settings( return PAM_SUCCESS; } -static int configure_runtime_directory( - pam_handle_t *handle, - UserRecord *ur, - const char *rt) { - - int r; - - assert(handle); - assert(ur); - assert(rt); - - if (!validate_runtime_directory(handle, rt, ur->uid)) - return PAM_SUCCESS; - - r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0); - if (r != PAM_SUCCESS) - return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set runtime dir: @PAMERR@"); - - return export_legacy_dbus_address(handle, rt); -} - static uint64_t pick_default_capability_ambient_set( UserRecord *ur, const char *service, @@ -1100,7 +1080,8 @@ static int register_session( SessionContext *c, UserRecord *ur, bool debug, - char **ret_seat) { + char **ret_seat, + char **ret_runtime_dir) { int r; @@ -1108,18 +1089,19 @@ static int register_session( assert(c); assert(ur); assert(ret_seat); + assert(ret_runtime_dir); /* We don't register session class none with logind */ if (streq(c->class, "none")) { pam_debug_syslog(handle, debug, "Skipping logind registration for session class none."); - *ret_seat = NULL; + *ret_seat = *ret_runtime_dir = NULL; return PAM_SUCCESS; } /* Make most of this a NOP on non-logind systems */ if (!logind_running()) { pam_debug_syslog(handle, debug, "Skipping logind registration as logind is not running."); - *ret_seat = NULL; + *ret_seat = *ret_runtime_dir = NULL; return PAM_SUCCESS; } @@ -1192,7 +1174,7 @@ static int register_session( if (streq_ptr(error_id, "io.systemd.Login.AlreadySessionMember")) { /* We are already in a session, don't do anything */ pam_debug_syslog(handle, debug, "Not creating session: %s", error_id); - *ret_seat = NULL; + *ret_seat = *ret_runtime_dir = NULL; return PAM_SUCCESS; } if (error_id) @@ -1289,7 +1271,7 @@ static int register_session( /* We are already in a session, don't do anything */ pam_debug_syslog(handle, debug, "Not creating session: %s", bus_error_message(&error, r)); - *ret_seat = NULL; + *ret_seat = *ret_runtime_dir = NULL; return PAM_SUCCESS; } @@ -1325,16 +1307,6 @@ static int register_session( if (r != PAM_SUCCESS) return r; - if (original_uid == ur->uid) { - /* Don't set $XDG_RUNTIME_DIR if the user we now authenticated for does not match the - * original user of the session. We do this in order not to result in privileged apps - * clobbering the runtime directory unnecessarily. */ - - r = configure_runtime_directory(handle, ur, runtime_path); - if (r != PAM_SUCCESS) - return r; - } - /* Most likely we got the session/type/class from environment variables, but might have gotten the data * somewhere else (for example PAM module parameters). Let's now update the environment variables, so that this * data is inherited into the session processes, and programs can rely on them to be initialized. */ @@ -1379,17 +1351,24 @@ static int register_session( TAKE_FD(fd); } + /* Don't set $XDG_RUNTIME_DIR if the user we now authenticated for does not match the + * original user of the session. We do this in order not to result in privileged apps + * clobbering the runtime directory unnecessarily. */ + _cleanup_free_ char *rt = NULL; + if (original_uid == ur->uid && validate_runtime_directory(handle, runtime_path, ur->uid)) + if (strdup_to(&rt, runtime_path) < 0) + return pam_log_oom(handle); + /* Everything worked, hence let's patch in the data we learned. Since 'real_set' points into the * D-Bus message, let's copy it and return it as a buffer */ - char *rs = NULL; - if (real_seat) { - rs = strdup(real_seat); - if (!rs) - return pam_log_oom(handle); - } + _cleanup_free_ char *rs = NULL; + if (strdup_to(&rs, real_seat) < 0) + return pam_log_oom(handle); - c->seat = *ret_seat = rs; c->vtnr = real_vtnr; + c->seat = *ret_seat = TAKE_PTR(rs); + *ret_runtime_dir = TAKE_PTR(rt); + return PAM_SUCCESS; } @@ -1414,9 +1393,103 @@ static int import_shell_credentials(pam_handle_t *handle) { return PAM_SUCCESS; } -static int update_home_env( +static int mkdir_chown_open_directory( + int parent_fd, + const char *name, + uid_t uid, + gid_t gid, + mode_t mode) { + + _cleanup_free_ char *t = NULL; + int r; + + assert(parent_fd >= 0); + assert(name); + assert(uid_is_valid(uid)); + assert(gid_is_valid(gid)); + assert(mode != MODE_INVALID); + + for (unsigned attempt = 0;; attempt++) { + _cleanup_close_ int fd = openat(parent_fd, name, O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (fd >= 0) + return TAKE_FD(fd); + if (errno != ENOENT) + return -errno; + + /* Let's create the directory under a temporary name first, since we want to make sure that + * once it appears under the right name it has the right ownership */ + r = tempfn_random(name, /* extra= */ NULL, &t); + if (r < 0) + return r; + + fd = open_mkdir_at(parent_fd, t, O_CLOEXEC|O_EXCL, 0700); /* Use restrictive mode until ownership is in order */ + if (fd < 0) + return fd; + + r = RET_NERRNO(fchown(fd, uid, gid)); + if (r < 0) + goto fail; + + r = RET_NERRNO(fchmod(fd, mode)); + if (r < 0) + goto fail; + + r = rename_noreplace(parent_fd, t, parent_fd, name); + if (r >= 0) + return TAKE_FD(fd); + if (r != -EEXIST || attempt >= 5) + goto fail; + + /* Maybe some other login attempt created the directory at the same time? Let's retry */ + (void) unlinkat(parent_fd, t, AT_REMOVEDIR); + t = mfree(t); + } + +fail: + (void) unlinkat(parent_fd, ASSERT_PTR(t), AT_REMOVEDIR); + return r; +} + +static int make_area_runtime_directory( pam_handle_t *handle, UserRecord *ur, + const char *runtime_directory, + const char *area, + char **ret) { + + assert(handle); + assert(ur); + assert(runtime_directory); + assert(area); + assert(ret); + + /* Let's be careful with creating these directories, the runtime directory is owned by the user after all, + * and they might play symlink games with us. */ + + _cleanup_close_ int fd = open(runtime_directory, O_CLOEXEC|O_PATH|O_DIRECTORY); + if (fd < 0) + return pam_syslog_errno(handle, LOG_ERR, errno, "Unable to open runtime directory '%s': %m", runtime_directory); + + _cleanup_close_ int fd_areas = mkdir_chown_open_directory(fd, "Areas", ur->uid, user_record_gid(ur), 0755); + if (fd_areas < 0) + return pam_syslog_errno(handle, LOG_ERR, fd_areas, "Unable to create 'Areas' directory below '%s': %m", runtime_directory); + + _cleanup_close_ int fd_area = mkdir_chown_open_directory(fd_areas, area, ur->uid, user_record_gid(ur), 0755); + if (fd_area < 0) + return pam_syslog_errno(handle, LOG_ERR, fd_area, "Unable to create '%s' directory below '%s/Areas': %m", area, runtime_directory); + + char *j = path_join(runtime_directory, "Areas", area); + if (!j) + return pam_log_oom(handle); + + *ret = j; + return 0; +} + +static int setup_environment( + pam_handle_t *handle, + UserRecord *ur, + const char *runtime_directory, const char *area, bool debug) { @@ -1430,7 +1503,7 @@ static int update_home_env( /* If an empty area string is specified, this means an explicit: do not use the area logic, normalize this here */ area = empty_to_null(area); - _cleanup_free_ char *ha = NULL; + _cleanup_free_ char *ha = NULL, *area_copy = NULL; if (area) { _cleanup_free_ char *j = path_join(h, "Areas", area); if (!j) @@ -1458,27 +1531,47 @@ static int update_home_env( pam_info(handle, "Path '%s' of requested user area '%s' is not owned by user, reverting to regular home directory.", ha, area); area = NULL; } else { - pam_debug_syslog(handle, debug, "Area '%s' selected, setting $HOME to '%s': %m", area, ha); + /* All good, now make a copy of the area string, since we quite likely are + * going to invalidate it (if it points into the environment block), via the + * update_environment() call below */ + area_copy = strdup(area); + if (!area_copy) + return pam_log_oom(handle); + + pam_debug_syslog(handle, debug, "Area '%s' selected, setting $HOME to '%s'.", area, ha); h = ha; + area = area_copy; } } } - if (area) { - r = update_environment(handle, "XDG_AREA", area); + r = update_environment(handle, "XDG_AREA", area); + if (r != PAM_SUCCESS) + return r; + + r = update_environment(handle, "HOME", h); + if (r != PAM_SUCCESS) + return r; + + _cleanup_free_ char *per_area_runtime_directory = NULL; + if (runtime_directory && area) { + /* Also create a per-area subdirectory for $XDG_RUNTIME_DIR, so that each area has their own + * set of runtime services. We follow the same directory structure as for $HOME. Note that we + * do not define any form of automatic clean-up for the per-aera subdirs beyond the regular + * clean-up of the whole $XDG_RUNTIME_DIRECTORY hierarchy when the user finally logs out. */ + + r = make_area_runtime_directory(handle, ur, runtime_directory, area, &per_area_runtime_directory); if (r != PAM_SUCCESS) return r; - } else if (pam_getenv(handle, "XDG_AREA")) { - /* Unset the $XDG_AREA variable if set. Note that pam_putenv() would log nastily behind our - * back if we call it without $XDG_AREA actually being set. Hence we check explicitly if it's - * set before. */ - r = pam_putenv(handle, "XDG_AREA"); - if (!IN_SET(r, PAM_SUCCESS, PAM_BAD_ITEM)) - pam_syslog_pam_error(handle, LOG_WARNING, r, - "Failed to unset XDG_AREA environment variable, ignoring: @PAMERR@"); + + runtime_directory = per_area_runtime_directory; } - return update_environment(handle, "HOME", h); + r = update_environment(handle, "XDG_RUNTIME_DIR", runtime_directory); + if (r != PAM_SUCCESS) + return r; + + return export_legacy_dbus_address(handle, runtime_directory); } _public_ PAM_EXTERN int pam_sm_open_session( @@ -1544,8 +1637,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( session_context_mangle(handle, &c, ur, debug); - _cleanup_free_ char *seat_buffer = NULL; - r = register_session(handle, &c, ur, debug, &seat_buffer); + _cleanup_free_ char *seat_buffer = NULL, *runtime_dir = NULL; + r = register_session(handle, &c, ur, debug, &seat_buffer, &runtime_dir); if (r != PAM_SUCCESS) return r; @@ -1553,7 +1646,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (r != PAM_SUCCESS) return r; - r = update_home_env(handle, ur, c.area, debug); + r = setup_environment(handle, ur, runtime_dir, c.area, debug); if (r != PAM_SUCCESS) return r; From 263ea6246839bde192fd25a3044436d01932e3f8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Feb 2025 12:08:16 +0100 Subject: [PATCH 7/8] test: extend test case to validate per-area $XDG_RUNTIME_DIR --- test/units/TEST-46-HOMED.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/units/TEST-46-HOMED.sh b/test/units/TEST-46-HOMED.sh index 78bfefa48d..5bab0b11b7 100755 --- a/test/units/TEST-46-HOMED.sh +++ b/test/units/TEST-46-HOMED.sh @@ -679,14 +679,18 @@ run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest ln -s test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $HOME')" = "/home/subareatest" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo x$XDG_AREA')" = "x" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $HOME')" = "/home/subareatest/Areas/furb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_AREA')" = "furb" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)/Areas/furb" PASSWORD=quux homectl update subareatest --default-area=molb test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $HOME')" = "/home/subareatest/Areas/molb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $XDG_AREA')" = "molb" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)/Areas/molb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $HOME')" = "/home/subareatest/Areas/furb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_AREA')" = "furb" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)/Areas/furb" # Install a PK rule that allows 'subareatest' user to invoke run0 without password, just for testing cat > /usr/share/polkit-1/rules.d/subareatest.rules <<'EOF' @@ -701,17 +705,22 @@ EOF # Test "recursive" operation test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb sh -c 'echo $HOME')" = "/home/subareatest/Areas/molb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb sh -c 'echo $XDG_AREA')" = "molb" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)/Areas/molb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $HOME')" = "/home/subareatest/Areas/furb" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_AREA')" = "furb" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)/Areas/furb" # Test symlinked area mkdir -p /home/srub chown subareatest:subareatest /home/srub test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a srub sh -c 'echo $HOME')" = "/home/srub" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a srub sh -c 'echo $XDG_AREA')" = "srub" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a srub sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)/Areas/srub" # Verify that login into an area not owned by target user will be redirected to main area +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a root sh -c 'echo $HOME')" = "/home/subareatest" test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a root sh -c 'echo x$XDG_AREA')" = "x" +test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a root sh -c 'echo $XDG_RUNTIME_DIR')" = "/run/user/$(id -u subareatest)" systemctl stop user@"$(id -u subareatest)".service From 4ec864162d848404f710a65cc35cc1b79f972859 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 24 Feb 2025 12:07:58 +0100 Subject: [PATCH 8/8] update TODO --- TODO | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TODO b/TODO index a3579141b2..223a8cb3e5 100644 --- a/TODO +++ b/TODO @@ -140,6 +140,11 @@ Features: them under various conditions: 1. if tpm2 is available or not available; 2. if sb is on or off; 3. if we are netbooted or not; … +* logind: invoke a service manager for "area" logins too. i.e. instantiate + user@.service also for logins where XDG_AREA is set, in per-area fashion, and + ref count it properly. Benefit: graphical logins should start working with + the area logic. + * repart: introduce concept of "ghost" partitions, that we setup in almost all ways like other partitions, but do not actually register in the actual gpt table, but only tell the kernel about via BLKPG ioctl. These partitions are