diff options
Diffstat (limited to '')
-rw-r--r-- | src/shared/pam-util.c | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c new file mode 100644 index 0000000..f5814ef --- /dev/null +++ b/src/shared/pam-util.c @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <security/pam_ext.h> +#include <syslog.h> +#include <stdlib.h> + +#include "alloc-util.h" +#include "bus-internal.h" +#include "errno-util.h" +#include "format-util.h" +#include "macro.h" +#include "pam-util.h" +#include "process-util.h" +#include "stdio-util.h" +#include "string-util.h" + +int pam_syslog_errno(pam_handle_t *handle, int level, int error, const char *format, ...) { + va_list ap; + + LOCAL_ERRNO(error); + + va_start(ap, format); + pam_vsyslog(handle, LOG_ERR, format, ap); + va_end(ap); + + return error == -ENOMEM ? PAM_BUF_ERR : PAM_SERVICE_ERR; +} + +int pam_syslog_pam_error(pam_handle_t *handle, int level, int error, const char *format, ...) { + /* This wraps pam_syslog() but will replace @PAMERR@ with a string from pam_strerror(). + * @PAMERR@ must be at the very end. */ + + va_list ap; + va_start(ap, format); + + const char *p = endswith(format, "@PAMERR@"); + if (p) { + const char *pamerr = pam_strerror(handle, error); + if (strchr(pamerr, '%')) + pamerr = "n/a"; /* We cannot have any formatting chars */ + + char buf[p - format + strlen(pamerr) + 1]; + xsprintf(buf, "%.*s%s", (int)(p - format), format, pamerr); + + DISABLE_WARNING_FORMAT_NONLITERAL; + pam_vsyslog(handle, level, buf, ap); + REENABLE_WARNING; + } else + pam_vsyslog(handle, level, format, ap); + + va_end(ap); + + return error; +} + +/* A small structure we store inside the PAM session object, that allows us to reuse bus connections but pins + * it to the process thoroughly. */ +struct PamBusData { + sd_bus *bus; + pam_handle_t *pam_handle; + char *cache_id; +}; + +static PamBusData *pam_bus_data_free(PamBusData *d) { + /* The actual destructor */ + if (!d) + return NULL; + + /* NB: PAM sessions usually involve forking off a child process, and thus the PAM context might be + * duplicated in the child. This destructor might be called twice: both in the parent and in the + * child. sd_bus_flush_close_unref() however is smart enough to be a NOP when invoked in any other + * process than the one it was invoked from, hence we don't need to add any extra protection here to + * ensure that destruction of the bus connection in the child affects the parent's connection + * somehow. */ + sd_bus_flush_close_unref(d->bus); + free(d->cache_id); + + /* Note: we don't destroy pam_handle here, because this object is pinned by the handle, and not vice versa! */ + + return mfree(d); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(PamBusData*, pam_bus_data_free); + +static void pam_bus_data_destroy(pam_handle_t *handle, void *data, int error_status) { + /* Destructor when called from PAM. Note that error_status is supposed to tell us via PAM_DATA_SILENT + * whether we are called in a forked off child of the PAM session or in the original parent. We don't + * bother with that however, and instead rely on the PID checks that sd_bus_flush_close_unref() does + * internally anyway. That said, we still generate a warning message, since this really shouldn't + * happen. */ + + if (!data) + return; + + PamBusData *d = data; + if (FLAGS_SET(error_status, PAM_DATA_SILENT) && + d->bus && bus_origin_changed(d->bus)) + /* Please adjust test/units/end.sh when updating the log message. */ + pam_syslog(handle, LOG_DEBUG, "Attempted to close sd-bus after fork whose connection is opened before the fork, this should not happen."); + + pam_bus_data_free(data); +} + +static char* pam_make_bus_cache_id(const char *module_name) { + char *id; + + /* We want to cache bus connections between hooks. But we don't want to allow them to be reused in + * child processes (because sd-bus doesn't support that). We also don't want them to be reused + * between our own PAM modules, because they might be linked against different versions of our + * utility functions and share different state. Hence include both a module ID and a PID in the data + * field ID. */ + + if (asprintf(&id, "system-bus-%s-" PID_FMT, ASSERT_PTR(module_name), getpid_cached()) < 0) + return NULL; + + return id; +} + +void pam_bus_data_disconnectp(PamBusData **_d) { + PamBusData *d = *ASSERT_PTR(_d); + pam_handle_t *handle; + int r; + + /* Disconnects the connection explicitly (for use via _cleanup_()) when called */ + + if (!d) + return; + + handle = ASSERT_PTR(d->pam_handle); /* Keep a reference to the session even after 'd' might be invalidated */ + + r = pam_set_data(handle, ASSERT_PTR(d->cache_id), NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to release PAM user record data, ignoring: @PAMERR@"); + + /* Note, the pam_set_data() call will invalidate 'd', don't access here anymore */ +} + +int pam_acquire_bus_connection( + pam_handle_t *handle, + const char *module_name, + sd_bus **ret_bus, + PamBusData **ret_pam_bus_data) { + + _cleanup_(pam_bus_data_freep) PamBusData *d = NULL; + _cleanup_free_ char *cache_id = NULL; + int r; + + assert(handle); + assert(module_name); + assert(ret_bus); + + cache_id = pam_make_bus_cache_id(module_name); + if (!cache_id) + return pam_log_oom(handle); + + /* We cache the bus connection so that we can share it between the session and the authentication hooks */ + r = pam_get_data(handle, cache_id, (const void**) &d); + if (r == PAM_SUCCESS && d) + goto success; + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get bus connection: @PAMERR@"); + + d = new(PamBusData, 1); + if (!d) + return pam_log_oom(handle); + + *d = (PamBusData) { + .cache_id = TAKE_PTR(cache_id), + .pam_handle = handle, + }; + + r = sd_bus_open_system(&d->bus); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to connect to system bus: %m"); + + r = pam_set_data(handle, d->cache_id, d, pam_bus_data_destroy); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM bus data: @PAMERR@"); + +success: + *ret_bus = sd_bus_ref(d->bus); + + if (ret_pam_bus_data) + *ret_pam_bus_data = d; + + TAKE_PTR(d); /* don't auto-destroy anymore, it's installed now */ + + return PAM_SUCCESS; +} + +int pam_release_bus_connection(pam_handle_t *handle, const char *module_name) { + _cleanup_free_ char *cache_id = NULL; + int r; + + assert(module_name); + + cache_id = pam_make_bus_cache_id(module_name); + if (!cache_id) + return pam_log_oom(handle); + + r = pam_set_data(handle, cache_id, NULL, NULL); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to release PAM user record data: @PAMERR@"); + + return PAM_SUCCESS; +} + +void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) { + /* A generic destructor for pam_set_data() that just frees the specified data */ + free(data); +} |