pam_systemd/pam_systemd_home: various fixes (#36505)

This commit is contained in:
Yu Watanabe
2025-02-27 12:37:05 +09:00
committed by GitHub
10 changed files with 234 additions and 105 deletions
+5
View File
@@ -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;
+13 -13
View File
@@ -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);
+187 -72
View File
@@ -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"
@@ -370,14 +371,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 +387,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@");
@@ -577,12 +586,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);
@@ -810,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,
@@ -1078,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;
@@ -1086,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;
}
@@ -1170,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)
@@ -1267,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;
}
@@ -1303,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. */
@@ -1357,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;
}
@@ -1392,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) {
@@ -1408,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)
@@ -1436,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(
@@ -1522,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;
@@ -1531,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;
+2 -3
View File
@@ -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
+1 -1
View File
@@ -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