summaryrefslogtreecommitdiffstats
path: root/src/shared/pam-util.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared/pam-util.c')
-rw-r--r--src/shared/pam-util.c211
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);
+}