1/*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2024 Beckhoff Automation GmbH & Co. KG
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 * SUCH DAMAGE.
26 */
27
28#include <sys/stat.h>
29#include <dirent.h>
30#include <errno.h>
31#include <fcntl.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35#include <unistd.h>
36#include <pwd.h>
37
38#define	PAM_SM_SESSION
39
40#include <security/pam_appl.h>
41#include <security/pam_modules.h>
42#include <security/pam_mod_misc.h>
43
44#define	BASE_RUNTIME_DIR_PREFIX	"/var/run/xdg"
45#define	RUNTIME_DIR_PREFIX	runtime_dir_prefix != NULL ? runtime_dir_prefix : BASE_RUNTIME_DIR_PREFIX
46
47#define	RUNTIME_DIR_PREFIX_MODE	0711
48#define	RUNTIME_DIR_MODE	0700	/* XDG spec */
49
50#define	XDG_MAX_SESSION		100 /* Arbitrary limit because we need one */
51
52static int
53_pam_xdg_open(pam_handle_t *pamh, int flags __unused,
54    int argc __unused, const char *argv[] __unused)
55{
56	struct passwd *passwd;
57	const char *user;
58	const char *runtime_dir_prefix;
59	struct stat sb;
60	char *runtime_dir = NULL;
61	char *xdg_session_file;
62	int rv, rt_dir_prefix, rt_dir, session_file, i;
63
64	session_file = -1;
65	rt_dir_prefix = -1;
66	runtime_dir_prefix = openpam_get_option(pamh, "runtime_dir_prefix");
67
68	/* Get user info */
69	rv = pam_get_item(pamh, PAM_USER, (const void **)&user);
70	if (rv != PAM_SUCCESS || user == NULL) {
71		PAM_VERBOSE_ERROR("Can't get user information");
72		goto out;
73	}
74	if ((passwd = getpwnam(user)) == NULL) {
75		PAM_VERBOSE_ERROR("Can't get user information");
76		rv = PAM_SESSION_ERR;
77		goto out;
78	}
79
80	/* Open or create the base xdg directory */
81	rt_dir_prefix = open(RUNTIME_DIR_PREFIX, O_DIRECTORY | O_NOFOLLOW);
82	if (rt_dir_prefix < 0) {
83		rt_dir_prefix = mkdir(RUNTIME_DIR_PREFIX, RUNTIME_DIR_PREFIX_MODE);
84		if (rt_dir_prefix != 0) {
85			PAM_VERBOSE_ERROR("Can't mkdir %s", RUNTIME_DIR_PREFIX);
86			rv = PAM_SESSION_ERR;
87			goto out;
88		}
89		rt_dir_prefix = open(RUNTIME_DIR_PREFIX, O_DIRECTORY | O_NOFOLLOW);
90	}
91
92	/* Open or create the user xdg directory */
93	rt_dir = openat(rt_dir_prefix, user, O_DIRECTORY | O_NOFOLLOW);
94	if (rt_dir < 0) {
95		rt_dir = mkdirat(rt_dir_prefix, user, RUNTIME_DIR_MODE);
96		if (rt_dir != 0) {
97			PAM_VERBOSE_ERROR("mkdir: %s/%s (%d)", RUNTIME_DIR_PREFIX, user, rt_dir);
98			rv = PAM_SESSION_ERR;
99			goto out;
100		}
101		rv = fchownat(rt_dir_prefix, user, passwd->pw_uid, passwd->pw_gid, 0);
102		if (rv != 0) {
103			PAM_VERBOSE_ERROR("fchownat: %s/%s (%d)", RUNTIME_DIR_PREFIX, user, rv);
104			rv = unlinkat(rt_dir_prefix, user, AT_REMOVEDIR);
105			if (rv == -1)
106				PAM_VERBOSE_ERROR("unlinkat: %s/%s (%d)", RUNTIME_DIR_PREFIX, user, errno);
107			rv = PAM_SESSION_ERR;
108			goto out;
109		}
110	} else {
111		/* Check that the already create dir is correctly owned */
112		rv = fstatat(rt_dir_prefix, user, &sb, 0);
113		if (rv == -1) {
114			PAM_VERBOSE_ERROR("fstatat %s/%s failed (%d)", RUNTIME_DIR_PREFIX, user, errno);
115			rv = PAM_SESSION_ERR;
116			goto out;
117		}
118		if (sb.st_uid != passwd->pw_uid ||
119		  sb.st_gid != passwd->pw_gid) {
120			PAM_VERBOSE_ERROR("%s/%s isn't owned by %d:%d\n", RUNTIME_DIR_PREFIX, user, passwd->pw_uid, passwd->pw_gid);
121			rv = PAM_SESSION_ERR;
122			goto out;
123		}
124		/* Test directory mode */
125		if ((sb.st_mode & 0x1FF) != RUNTIME_DIR_MODE) {
126			PAM_VERBOSE_ERROR("%s/%s have wrong mode\n", RUNTIME_DIR_PREFIX, user);
127			rv = PAM_SESSION_ERR;
128			goto out;
129		}
130	}
131
132	/* Setup the environment variable */
133	rv = asprintf(&runtime_dir, "XDG_RUNTIME_DIR=%s/%s", RUNTIME_DIR_PREFIX, user);
134	if (rv < 0) {
135		PAM_VERBOSE_ERROR("asprintf failed %d\n", rv);
136		rv = PAM_SESSION_ERR;
137		goto out;
138	}
139	rv = pam_putenv(pamh, runtime_dir);
140	if (rv != PAM_SUCCESS) {
141		PAM_VERBOSE_ERROR("pam_putenv: failed (%d)", rv);
142		rv = PAM_SESSION_ERR;
143		goto out;
144	}
145
146	/* Setup the session count file */
147	for (i = 0; i < XDG_MAX_SESSION; i++) {
148		rv = asprintf(&xdg_session_file, "%s/xdg_session.%d", user, i);
149		if (rv < 0) {
150			PAM_VERBOSE_ERROR("asprintf failed %d\n", rv);
151			rv = PAM_SESSION_ERR;
152			goto out;
153		}
154		rv = 0;
155		session_file = openat(rt_dir_prefix, xdg_session_file, O_CREAT | O_EXCL, RUNTIME_DIR_MODE);
156		free(xdg_session_file);
157		if (session_file >= 0)
158			break;
159	}
160	if (session_file < 0) {
161		PAM_VERBOSE_ERROR("Too many sessions");
162		rv = PAM_SESSION_ERR;
163		goto out;
164	}
165
166out:
167	if (session_file >= 0)
168		close(session_file);
169	if (rt_dir_prefix >= 0)
170		close(rt_dir_prefix);
171
172	if (runtime_dir)
173		free(runtime_dir);
174	return (rv);
175}
176
177static int
178remove_dir(int fd)
179{
180	DIR *dirp;
181	struct dirent *dp;
182
183	dirp = fdopendir(fd);
184	if (dirp == NULL)
185		return (-1);
186
187	while ((dp = readdir(dirp)) != NULL) {
188		if (dp->d_type == DT_DIR) {
189			int dirfd;
190
191			if (strcmp(dp->d_name, ".") == 0 ||
192			    strcmp(dp->d_name, "..") == 0)
193				continue;
194			dirfd = openat(fd, dp->d_name, 0);
195			remove_dir(dirfd);
196			close(dirfd);
197			unlinkat(fd, dp->d_name, AT_REMOVEDIR);
198			continue;
199		}
200		unlinkat(fd, dp->d_name, 0);
201	}
202	closedir(dirp);
203
204	return (0);
205}
206
207static int
208_pam_xdg_close(pam_handle_t *pamh __unused, int flags __unused,
209    int argc __unused, const char *argv[] __unused)
210{
211	struct passwd *passwd;
212	const char *user;
213	const char *runtime_dir_prefix;
214	struct stat sb;
215	char *xdg_session_file;
216	int rv, rt_dir_prefix, rt_dir, session_file, i;
217
218	rt_dir = -1;
219	rt_dir_prefix = -1;
220	runtime_dir_prefix = openpam_get_option(pamh, "runtime_dir_prefix");
221
222	/* Get user info */
223	rv = pam_get_item(pamh, PAM_USER, (const void **)&user);
224	if (rv != PAM_SUCCESS || user == NULL) {
225		PAM_VERBOSE_ERROR("Can't get user information");
226		goto out;
227	}
228	if ((passwd = getpwnam(user)) == NULL) {
229		PAM_VERBOSE_ERROR("Can't get user information");
230		rv = PAM_SESSION_ERR;
231		goto out;
232	}
233
234	/* Open the xdg base directory */
235	rt_dir_prefix = open(RUNTIME_DIR_PREFIX, O_DIRECTORY | O_NOFOLLOW);
236	if (rt_dir_prefix < 0) {
237		PAM_VERBOSE_ERROR("open: %s failed (%d)\n", runtime_dir_prefix, rt_dir_prefix);
238		rv = PAM_SESSION_ERR;
239		goto out;
240	}
241	/* Check that the already created dir is correctly owned */
242	rv = fstatat(rt_dir_prefix, user, &sb, 0);
243	if (rv == -1) {
244		PAM_VERBOSE_ERROR("fstatat %s/%s failed (%d)", RUNTIME_DIR_PREFIX, user, errno);
245		rv = PAM_SESSION_ERR;
246		goto out;
247	}
248	if (sb.st_uid != passwd->pw_uid ||
249	    sb.st_gid != passwd->pw_gid) {
250		PAM_VERBOSE_ERROR("%s/%s isn't owned by %d:%d\n", RUNTIME_DIR_PREFIX, user, passwd->pw_uid, passwd->pw_gid);
251		rv = PAM_SESSION_ERR;
252		goto out;
253	}
254	/* Test directory mode */
255	if ((sb.st_mode & 0x1FF) != RUNTIME_DIR_MODE) {
256		PAM_VERBOSE_ERROR("%s/%s have wrong mode\n", RUNTIME_DIR_PREFIX, user);
257		rv = PAM_SESSION_ERR;
258		goto out;
259	}
260
261	/* Open the user xdg directory */
262	rt_dir = openat(rt_dir_prefix, user, O_DIRECTORY | O_NOFOLLOW);
263	if (rt_dir < 0) {
264		PAM_VERBOSE_ERROR("openat: %s/%s failed (%d)\n", RUNTIME_DIR_PREFIX, user, rt_dir_prefix);
265		rv = PAM_SESSION_ERR;
266		goto out;
267	}
268
269	/* Get the last session file created */
270	for (i = XDG_MAX_SESSION; i >= 0; i--) {
271		rv = asprintf(&xdg_session_file, "%s/xdg_session.%d", user, i);
272		if (rv < 0) {
273			PAM_VERBOSE_ERROR("asprintf failed %d\n", rv);
274			rv = PAM_SESSION_ERR;
275			goto out;
276		}
277		rv = 0;
278		session_file = openat(rt_dir_prefix, xdg_session_file, 0);
279		if (session_file >= 0) {
280			unlinkat(rt_dir_prefix, xdg_session_file, 0);
281			free(xdg_session_file);
282			break;
283		}
284		free(xdg_session_file);
285	}
286	if (session_file < 0) {
287		PAM_VERBOSE_ERROR("Can't find session number\n");
288		rv = PAM_SESSION_ERR;
289		goto out;
290	}
291	close(session_file);
292
293	/* Final cleanup if last user session */
294	if (i == 0) {
295		remove_dir(rt_dir);
296		if (unlinkat(rt_dir_prefix, user, AT_REMOVEDIR) != 0) {
297			PAM_VERBOSE_ERROR("Can't cleanup %s/%s\n", runtime_dir_prefix, user);
298			rv = PAM_SESSION_ERR;
299			goto out;
300		}
301	}
302
303	rv = PAM_SUCCESS;
304out:
305	if (rt_dir >= 0)
306		close(rt_dir);
307	if (rt_dir_prefix >= 0)
308		close(rt_dir_prefix);
309	return (rv);
310}
311
312PAM_EXTERN int
313pam_sm_open_session(pam_handle_t *pamh, int flags,
314    int argc, const char *argv[])
315{
316
317	return (_pam_xdg_open(pamh, flags, argc, argv));
318}
319
320PAM_EXTERN int
321pam_sm_close_session(pam_handle_t *pamh, int flags,
322    int argc, const char *argv[])
323{
324
325	return (_pam_xdg_close(pamh, flags, argc, argv));
326}
327
328PAM_MODULE_ENTRY("pam_xdg");
329