summaryrefslogtreecommitdiffstats
path: root/src/shared/pam-util.c
blob: 82e897ee53ce5021e25dca2f2f24b65ea15fd89c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <security/pam_ext.h>
#include <syslog.h>
#include <stdlib.h>

#include "alloc-util.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 resue 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 (error_status & PAM_DATA_SILENT)
                pam_syslog(handle, LOG_WARNING, "Attempted to close sd-bus after 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);
}