]>
Commit | Line | Data |
---|---|---|
8c6db833 LP |
1 | /*-*- Mode: C; c-basic-offset: 8 -*-*/ |
2 | ||
3 | /*** | |
4 | This file is part of systemd. | |
5 | ||
6 | Copyright 2010 Lennart Poettering | |
7 | ||
8 | systemd is free software; you can redistribute it and/or modify it | |
9 | under the terms of the GNU General Public License as published by | |
10 | the Free Software Foundation; either version 2 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | systemd is distributed in the hope that it will be useful, but | |
14 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
16 | General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU General Public License | |
19 | along with systemd; If not, see <http://www.gnu.org/licenses/>. | |
20 | ***/ | |
21 | ||
22 | #include <errno.h> | |
23 | #include <fcntl.h> | |
24 | #include <sys/file.h> | |
25 | #include <pwd.h> | |
a838e6a1 | 26 | #include <endian.h> |
8c6db833 LP |
27 | |
28 | #include <security/pam_modules.h> | |
29 | #include <security/_pam_macros.h> | |
30 | #include <security/pam_modutil.h> | |
31 | #include <security/pam_ext.h> | |
32 | #include <security/pam_misc.h> | |
33 | ||
34 | #include <libcgroup.h> | |
35 | ||
36 | #include "util.h" | |
37 | #include "cgroup-util.h" | |
38 | #include "macro.h" | |
39 | #include "sd-daemon.h" | |
40 | ||
41 | static int parse_argv(pam_handle_t *handle, | |
42 | int argc, const char **argv, | |
43 | bool *create_session, | |
44 | bool *kill_session, | |
45 | bool *kill_user) { | |
46 | ||
47 | unsigned i; | |
48 | ||
49 | assert(argc >= 0); | |
50 | assert(argc == 0 || argv); | |
51 | ||
52 | for (i = 0; i < (unsigned) argc; i++) { | |
53 | int k; | |
54 | ||
55 | if (startswith(argv[i], "create-session=")) { | |
56 | if ((k = parse_boolean(argv[i] + 15)) < 0) { | |
57 | pam_syslog(handle, LOG_ERR, "Failed to parse create-session= argument."); | |
58 | return k; | |
59 | } | |
60 | ||
61 | if (create_session) | |
62 | *create_session = k; | |
63 | } else if (startswith(argv[i], "kill-session=")) { | |
64 | if ((k = parse_boolean(argv[i] + 13)) < 0) { | |
65 | pam_syslog(handle, LOG_ERR, "Failed to parse kill-session= argument."); | |
66 | return k; | |
67 | } | |
68 | ||
69 | if (kill_session) | |
70 | *kill_session = k; | |
71 | ||
72 | } else if (startswith(argv[i], "kill-user=")) { | |
73 | if ((k = parse_boolean(argv[i] + 10)) < 0) { | |
74 | pam_syslog(handle, LOG_ERR, "Failed to parse kill-user= argument."); | |
75 | return k; | |
76 | } | |
77 | ||
78 | if (kill_user) | |
79 | *kill_user = k; | |
80 | } else { | |
81 | pam_syslog(handle, LOG_ERR, "Unknown parameter '%s'.", argv[i]); | |
82 | return -EINVAL; | |
83 | } | |
84 | } | |
85 | ||
86 | if (kill_session && *kill_session && kill_user) | |
87 | *kill_user = true; | |
88 | ||
89 | return 0; | |
90 | } | |
91 | ||
92 | static int open_file_and_lock(const char *fn) { | |
93 | int fd; | |
94 | ||
95 | assert(fn); | |
96 | ||
97 | if ((fd = open(fn, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_CREAT, 0600)) < 0) | |
98 | return -errno; | |
99 | ||
2e225d65 LP |
100 | /* The BSD socket semantics are a lot nicer than those of |
101 | * POSIX locks. Which is why we use flock() here. BSD locking | |
102 | * does not work across NFS which however is not needed here | |
103 | * as the filesystems in question should be local, and only | |
104 | * locally accessible, and most likely even tmpfs. */ | |
105 | ||
8c6db833 LP |
106 | if (flock(fd, LOCK_EX) < 0) |
107 | return -errno; | |
108 | ||
109 | return fd; | |
110 | } | |
111 | ||
2e225d65 LP |
112 | enum { |
113 | SESSION_ID_AUDIT = 'a', | |
114 | SESSION_ID_COUNTER = 'c', | |
115 | SESSION_ID_RANDOM = 'r' | |
116 | }; | |
117 | ||
118 | static uint64_t get_session_id(int *mode) { | |
a838e6a1 LP |
119 | char *s; |
120 | int fd; | |
8c6db833 | 121 | |
2e225d65 LP |
122 | assert(mode); |
123 | ||
a838e6a1 LP |
124 | /* First attempt: let's use the session ID of the audit |
125 | * system, if it is available. */ | |
126 | if (read_one_line_file("/proc/self/sessionid", &s) >= 0) { | |
127 | uint32_t u; | |
128 | int r; | |
8c6db833 | 129 | |
a838e6a1 LP |
130 | r = safe_atou32(s, &u); |
131 | free(s); | |
8c6db833 | 132 | |
2e225d65 LP |
133 | if (r >= 0 && u != (uint32_t) -1) { |
134 | *mode = SESSION_ID_AUDIT; | |
a838e6a1 | 135 | return (uint64_t) u; |
2e225d65 | 136 | } |
a838e6a1 | 137 | } |
8c6db833 | 138 | |
a838e6a1 LP |
139 | /* Second attempt, use our own counter. */ |
140 | if ((fd = open_file_and_lock(RUNTIME_DIR "/user/.pam-systemd-session")) >= 0) { | |
141 | uint64_t counter; | |
142 | ssize_t r; | |
8c6db833 | 143 | |
a838e6a1 LP |
144 | /* We do a bit of endianess swapping here, just to be |
145 | * sure. /var should be machine specific anyway, and | |
146 | * /var/run even mounted from tmpfs, so this | |
147 | * byteswapping should really not be necessary. But | |
148 | * then again, you never know, so let's avoid any | |
149 | * risk. */ | |
8c6db833 | 150 | |
a838e6a1 LP |
151 | if (loop_read(fd, &counter, sizeof(counter), false) != sizeof(counter)) |
152 | counter = 1; | |
153 | else | |
154 | counter = le64toh(counter) + 1; | |
8c6db833 | 155 | |
a838e6a1 LP |
156 | if (lseek(fd, 0, SEEK_SET) == 0) { |
157 | uint64_t swapped = htole64(counter); | |
158 | ||
159 | r = loop_write(fd, &swapped, sizeof(swapped), false); | |
160 | ||
161 | if (r != sizeof(swapped)) | |
162 | r = -EIO; | |
163 | } else | |
164 | r = -errno; | |
8c6db833 | 165 | |
a838e6a1 LP |
166 | close_nointr_nofail(fd); |
167 | ||
2e225d65 LP |
168 | if (r >= 0) { |
169 | *mode = SESSION_ID_COUNTER; | |
a838e6a1 | 170 | return counter; |
2e225d65 | 171 | } |
a838e6a1 LP |
172 | } |
173 | ||
2e225d65 LP |
174 | *mode = SESSION_ID_RANDOM; |
175 | ||
a838e6a1 LP |
176 | /* Last attempt, pick a random value */ |
177 | return (uint64_t) random_ull(); | |
178 | } | |
8c6db833 LP |
179 | static int get_user_data( |
180 | pam_handle_t *handle, | |
181 | const char **ret_username, | |
182 | struct passwd **ret_pw) { | |
183 | ||
184 | const char *username; | |
185 | struct passwd *pw; | |
186 | int r; | |
187 | ||
188 | assert(handle); | |
189 | assert(ret_username); | |
190 | assert(ret_pw); | |
191 | ||
192 | if ((r = pam_get_user(handle, &username, NULL)) != PAM_SUCCESS) { | |
193 | pam_syslog(handle, LOG_ERR, "Failed to get user name."); | |
194 | return r; | |
195 | } | |
196 | ||
197 | if (!username || !*username) { | |
198 | pam_syslog(handle, LOG_ERR, "User name not valid."); | |
199 | return PAM_AUTH_ERR; | |
200 | } | |
201 | ||
202 | if (!(pw = pam_modutil_getpwnam(handle, username))) { | |
203 | pam_syslog(handle, LOG_ERR, "Failed to get user data."); | |
204 | return PAM_USER_UNKNOWN; | |
205 | } | |
206 | ||
207 | *ret_pw = pw; | |
208 | *ret_username = username; | |
209 | ||
210 | return PAM_SUCCESS; | |
211 | } | |
212 | ||
213 | static int create_user_group(pam_handle_t *handle, const char *group, struct passwd *pw, bool attach) { | |
214 | int r; | |
215 | ||
216 | assert(handle); | |
217 | assert(group); | |
218 | ||
219 | if (attach) | |
c6c18be3 | 220 | r = cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, group, 0); |
8c6db833 | 221 | else |
c6c18be3 | 222 | r = cg_create(SYSTEMD_CGROUP_CONTROLLER, group); |
8c6db833 LP |
223 | |
224 | if (r < 0) { | |
225 | pam_syslog(handle, LOG_ERR, "Failed to create cgroup: %s", strerror(-r)); | |
226 | return PAM_SESSION_ERR; | |
227 | } | |
228 | ||
c6c18be3 LP |
229 | if ((r = cg_set_task_access(SYSTEMD_CGROUP_CONTROLLER, group, 0755, pw->pw_uid, pw->pw_gid)) < 0 || |
230 | (r = cg_set_group_access(SYSTEMD_CGROUP_CONTROLLER, group, 0755, pw->pw_uid, pw->pw_gid)) < 0) { | |
8c6db833 LP |
231 | pam_syslog(handle, LOG_ERR, "Failed to change access modes: %s", strerror(-r)); |
232 | return PAM_SESSION_ERR; | |
233 | } | |
234 | ||
235 | return PAM_SUCCESS; | |
236 | } | |
237 | ||
238 | _public_ PAM_EXTERN int pam_sm_open_session( | |
239 | pam_handle_t *handle, | |
240 | int flags, | |
241 | int argc, const char **argv) { | |
242 | ||
243 | const char *username = NULL; | |
244 | struct passwd *pw; | |
245 | int r; | |
246 | char *buf = NULL; | |
247 | int lock_fd = -1; | |
248 | bool create_session = true; | |
249 | ||
250 | assert(handle); | |
251 | ||
252 | pam_syslog(handle, LOG_INFO, "pam-systemd initializing"); | |
253 | ||
254 | if (parse_argv(handle, argc, argv, &create_session, NULL, NULL) < 0) | |
255 | return PAM_SESSION_ERR; | |
256 | ||
257 | /* Make this a NOP on non-systemd systems */ | |
258 | if (sd_booted() <= 0) | |
259 | return PAM_SUCCESS; | |
260 | ||
c6c18be3 LP |
261 | if ((r = cg_init()) < 0) { |
262 | pam_syslog(handle, LOG_ERR, "libcgroup initialization failed: %s", strerror(-r)); | |
8c6db833 LP |
263 | r = PAM_SESSION_ERR; |
264 | goto finish; | |
265 | } | |
266 | ||
267 | if ((r = get_user_data(handle, &username, &pw)) != PAM_SUCCESS) | |
268 | goto finish; | |
269 | ||
270 | if (safe_mkdir(RUNTIME_DIR "/user", 0755, 0, 0) < 0) { | |
271 | pam_syslog(handle, LOG_ERR, "Failed to create runtime directory: %m"); | |
272 | r = PAM_SYSTEM_ERR; | |
273 | goto finish; | |
274 | } | |
275 | ||
276 | if ((lock_fd = open_file_and_lock(RUNTIME_DIR "/user/.pam-systemd-lock")) < 0) { | |
277 | pam_syslog(handle, LOG_ERR, "Failed to lock runtime directory: %m"); | |
278 | r = PAM_SYSTEM_ERR; | |
279 | goto finish; | |
280 | } | |
281 | ||
282 | /* Create /var/run/$USER */ | |
283 | free(buf); | |
284 | if (asprintf(&buf, RUNTIME_DIR "/user/%s", username) < 0) { | |
285 | r = PAM_BUF_ERR; | |
286 | goto finish; | |
287 | } | |
288 | ||
289 | if (safe_mkdir(buf, 0700, pw->pw_uid, pw->pw_gid) < 0) { | |
290 | pam_syslog(handle, LOG_WARNING, "Failed to create runtime directory: %m"); | |
291 | r = PAM_SYSTEM_ERR; | |
292 | goto finish; | |
293 | } else if ((r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", buf, 0)) != PAM_SUCCESS) { | |
294 | pam_syslog(handle, LOG_ERR, "Failed to set runtime dir."); | |
295 | goto finish; | |
296 | } | |
297 | ||
298 | free(buf); | |
299 | buf = NULL; | |
300 | ||
301 | if (create_session) { | |
a838e6a1 | 302 | const char *id; |
8c6db833 LP |
303 | |
304 | /* Reuse or create XDG session ID */ | |
a838e6a1 | 305 | if (!(id = pam_getenv(handle, "XDG_SESSION_ID"))) { |
2e225d65 | 306 | int mode; |
a838e6a1 | 307 | |
2e225d65 | 308 | if (asprintf(&buf, "%llux", (unsigned long long) get_session_id(&mode)) < 0) { |
8c6db833 LP |
309 | r = PAM_BUF_ERR; |
310 | goto finish; | |
311 | } | |
312 | ||
2e225d65 LP |
313 | /* To avoid id clashes we add the session id |
314 | * source to our session ids. Note that the | |
315 | * session id source might change during | |
316 | * runtime, because a filesystem became | |
317 | * writable or the system reconfigured. */ | |
318 | buf[strlen(buf)-1] = | |
319 | mode != SESSION_ID_AUDIT ? (char) mode : 0; | |
320 | ||
a838e6a1 LP |
321 | if ((r = pam_misc_setenv(handle, "XDG_SESSION_ID", buf, 0)) != PAM_SUCCESS) { |
322 | pam_syslog(handle, LOG_ERR, "Failed to set session id."); | |
8c6db833 LP |
323 | goto finish; |
324 | } | |
325 | ||
a838e6a1 LP |
326 | if (!(id = pam_getenv(handle, "XDG_SESSION_ID"))) { |
327 | pam_syslog(handle, LOG_ERR, "Failed to get session id."); | |
8c6db833 LP |
328 | r = PAM_SESSION_ERR; |
329 | goto finish; | |
330 | } | |
331 | } | |
332 | ||
a838e6a1 | 333 | r = asprintf(&buf, "/user/%s/%s", username, id); |
8c6db833 LP |
334 | } else |
335 | r = asprintf(&buf, "/user/%s/no-session", username); | |
336 | ||
337 | if (r < 0) { | |
338 | r = PAM_BUF_ERR; | |
339 | goto finish; | |
340 | } | |
341 | ||
342 | if ((r = create_user_group(handle, buf, pw, true)) != PAM_SUCCESS) | |
343 | goto finish; | |
344 | ||
345 | r = PAM_SUCCESS; | |
346 | ||
347 | finish: | |
348 | free(buf); | |
349 | ||
350 | if (lock_fd >= 0) | |
351 | close_nointr_nofail(lock_fd); | |
352 | ||
353 | return r; | |
354 | } | |
355 | ||
356 | static int session_remains(pam_handle_t *handle, const char *user_path) { | |
357 | struct cgroup_file_info info; | |
358 | int level = 0, r; | |
359 | void *iterator = NULL; | |
360 | bool remains = false; | |
361 | ||
362 | zero(info); | |
363 | ||
c6c18be3 | 364 | r = cgroup_walk_tree_begin(SYSTEMD_CGROUP_CONTROLLER, user_path, 0, &iterator, &info, &level); |
8c6db833 LP |
365 | while (r == 0) { |
366 | ||
367 | if (info.type != CGROUP_FILE_TYPE_DIR) | |
368 | goto next; | |
369 | ||
370 | if (streq(info.path, "")) | |
371 | goto next; | |
372 | ||
373 | if (streq(info.path, "no-session")) | |
374 | goto next; | |
375 | ||
376 | remains = true; | |
377 | break; | |
378 | ||
379 | next: | |
380 | ||
381 | r = cgroup_walk_tree_next(0, &iterator, &info, level); | |
382 | } | |
383 | ||
384 | ||
385 | if (remains) | |
386 | r = 1; | |
387 | else if (r == 0 || r == ECGEOF) | |
388 | r = 0; | |
389 | else | |
390 | r = cg_translate_error(r, errno); | |
391 | ||
392 | assert_se(cgroup_walk_tree_end(&iterator) == 0); | |
393 | ||
394 | return r; | |
395 | } | |
396 | ||
397 | _public_ PAM_EXTERN int pam_sm_close_session( | |
398 | pam_handle_t *handle, | |
399 | int flags, | |
400 | int argc, const char **argv) { | |
401 | ||
402 | const char *username = NULL; | |
403 | bool kill_session = false; | |
404 | bool kill_user = false; | |
405 | int lock_fd = -1, r; | |
406 | char *session_path = NULL, *nosession_path = NULL, *user_path = NULL; | |
a838e6a1 | 407 | const char *id; |
8c6db833 LP |
408 | struct passwd *pw; |
409 | ||
410 | assert(handle); | |
411 | ||
412 | if (parse_argv(handle, argc, argv, NULL, &kill_session, &kill_user) < 0) | |
413 | return PAM_SESSION_ERR; | |
414 | ||
415 | /* Make this a NOP on non-systemd systems */ | |
416 | if (sd_booted() <= 0) | |
417 | return PAM_SUCCESS; | |
418 | ||
419 | if ((r = get_user_data(handle, &username, &pw)) != PAM_SUCCESS) | |
420 | goto finish; | |
421 | ||
422 | if ((lock_fd = open_file_and_lock(RUNTIME_DIR "/user/.pam-systemd-lock")) < 0) { | |
423 | pam_syslog(handle, LOG_ERR, "Failed to lock runtime directory: %m"); | |
424 | r = PAM_SYSTEM_ERR; | |
425 | goto finish; | |
426 | } | |
427 | ||
428 | if (asprintf(&user_path, "/user/%s", username) < 0) { | |
429 | r = PAM_BUF_ERR; | |
430 | goto finish; | |
431 | } | |
432 | ||
a838e6a1 | 433 | if ((id = pam_getenv(handle, "XDG_SESSION_ID"))) { |
8c6db833 | 434 | |
a838e6a1 | 435 | if (asprintf(&session_path, "/user/%s/%s", username, id) < 0 || |
8c6db833 LP |
436 | asprintf(&nosession_path, "/user/%s/no-session", username) < 0) { |
437 | r = PAM_BUF_ERR; | |
438 | goto finish; | |
439 | } | |
440 | ||
441 | if (kill_session) { | |
8c6db833 | 442 | /* Kill processes in session cgroup */ |
c6c18be3 | 443 | if ((r = cg_kill_recursive_and_wait(SYSTEMD_CGROUP_CONTROLLER, session_path)) < 0) |
8c6db833 LP |
444 | pam_syslog(handle, LOG_ERR, "Failed to kill session cgroup: %s", strerror(-r)); |
445 | ||
8c6db833 LP |
446 | } else { |
447 | /* Migrate processes from session to | |
448 | * no-session cgroup. First, try to create the | |
449 | * no-session group in case it doesn't exist | |
450 | * yet. */ | |
451 | create_user_group(handle, nosession_path, pw, 0); | |
452 | ||
c6c18be3 | 453 | if ((r = cg_migrate_recursive(SYSTEMD_CGROUP_CONTROLLER, session_path, nosession_path, false)) < 0) |
8c6db833 LP |
454 | pam_syslog(handle, LOG_ERR, "Failed to migrate session cgroup: %s", strerror(-r)); |
455 | } | |
456 | ||
457 | /* Delete session cgroup */ | |
458 | if (r < 0) | |
459 | pam_syslog(handle, LOG_INFO, "Couldn't empty session cgroup, not deleting."); | |
460 | else { | |
c6c18be3 | 461 | if ((r = cg_delete(SYSTEMD_CGROUP_CONTROLLER, session_path)) < 0) |
8c6db833 LP |
462 | pam_syslog(handle, LOG_ERR, "Failed to delete session cgroup: %s", strerror(-r)); |
463 | } | |
464 | } | |
465 | ||
466 | /* GC user tree */ | |
c6c18be3 | 467 | cg_trim(SYSTEMD_CGROUP_CONTROLLER, user_path, false); |
8c6db833 LP |
468 | |
469 | if ((r = session_remains(handle, user_path)) < 0) | |
470 | pam_syslog(handle, LOG_ERR, "Failed to determine whether a session remains: %s", strerror(-r)); | |
471 | ||
472 | /* Kill user processes not attached to any session */ | |
473 | if (kill_user && r == 0) { | |
474 | ||
475 | /* Kill no-session cgroup */ | |
c6c18be3 | 476 | if ((r = cg_kill_recursive_and_wait(SYSTEMD_CGROUP_CONTROLLER, user_path)) < 0) |
8c6db833 LP |
477 | pam_syslog(handle, LOG_ERR, "Failed to kill user cgroup: %s", strerror(-r)); |
478 | } else { | |
479 | ||
c6c18be3 | 480 | if ((r = cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, user_path, true)) < 0) |
8c6db833 LP |
481 | pam_syslog(handle, LOG_ERR, "Failed to check user cgroup: %s", strerror(-r)); |
482 | ||
483 | /* If we managed to kill somebody, don't cleanup the cgroup. */ | |
484 | if (r == 0) | |
485 | r = -EBUSY; | |
486 | } | |
487 | ||
488 | if (r >= 0) { | |
489 | const char *runtime_dir; | |
490 | ||
491 | /* Remove user cgroup */ | |
c6c18be3 | 492 | if ((r = cg_delete(SYSTEMD_CGROUP_CONTROLLER, user_path)) < 0) |
8c6db833 LP |
493 | pam_syslog(handle, LOG_ERR, "Failed to delete user cgroup: %s", strerror(-r)); |
494 | ||
495 | /* This will migrate us to the /user cgroup. */ | |
496 | ||
497 | if ((runtime_dir = pam_getenv(handle, "XDG_RUNTIME_DIR"))) | |
498 | if ((r = rm_rf(runtime_dir, false, true)) < 0) | |
499 | pam_syslog(handle, LOG_ERR, "Failed to remove runtime directory: %s", strerror(-r)); | |
500 | } | |
501 | ||
502 | r = PAM_SUCCESS; | |
503 | ||
504 | finish: | |
505 | if (lock_fd >= 0) | |
506 | close_nointr_nofail(lock_fd); | |
507 | ||
508 | free(session_path); | |
509 | free(nosession_path); | |
510 | free(user_path); | |
511 | ||
512 | return r; | |
513 | } |