]> git.proxmox.com Git - mirror_lxcfs.git/blob - pam/pam_cgfs.c
pam: don't let add_controller fail
[mirror_lxcfs.git] / pam / pam_cgfs.c
1 /* pam-cgfs
2 *
3 * Copyright © 2016 Canonical, Inc
4 * Author: Serge Hallyn <serge.hallyn@ubuntu.com>
5 *
6 * When a user logs in, this pam module will create cgroups which
7 * the user may administer, for all controllers except name=systemd,
8 * or for any controllers listed on the command line (if any are
9 * listed).
10 *
11 * The cgroup created will be "user/$user/0" for the first session,
12 * "user/$user/1" for the second, etc.
13 *
14 * All requested cgroups must be mounted under /sys/fs/cgroup/$controller,
15 * no messing around with finding mountpoints.
16 *
17 * See COPYING file for details.
18 */
19
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <unistd.h>
23 #include <syslog.h>
24 #include <stdarg.h>
25 #include <errno.h>
26 #include <sys/mount.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #include <sys/param.h>
30 #include <pwd.h>
31 #include <stdbool.h>
32 #include <dirent.h>
33
34 #define PAM_SM_SESSION
35 #include <security/_pam_macros.h>
36 #include <security/pam_modules.h>
37
38 #include <linux/unistd.h>
39
40 static bool initialized;
41
42 static void mysyslog(int err, const char *format, ...)
43 {
44 va_list args;
45
46 va_start(args, format);
47 openlog("PAM-CGFS", LOG_CONS|LOG_PID, LOG_AUTH);
48 vsyslog(err, format, args);
49 va_end(args);
50 closelog();
51 }
52
53 static char *must_strcat(const char *first, ...) __attribute__((sentinel));
54
55 static char *must_strcat(const char *first, ...)
56 {
57 va_list args;
58 char *dest, *cur, *new;
59 size_t len;
60
61 do {
62 dest = strdup(first);
63 } while (!dest);
64 len = strlen(dest);
65
66 va_start(args, first);
67
68 while ((cur = va_arg(args, char *)) != NULL) {
69 size_t newlen = len + strlen(cur);
70 do {
71 new = realloc(dest, newlen + 1);
72 } while (!new);
73 dest = new;
74 strcat(dest, cur);
75 len = newlen;
76 }
77 va_end(args);
78
79 return dest;
80 }
81
82 static bool exists(const char *path)
83 {
84 struct stat sb;
85 int ret;
86
87 ret = stat(path, &sb);
88 return ret == 0;
89 }
90
91 static bool is_dir(const char *path)
92 {
93 struct stat sb;
94
95 if (stat(path, &sb) < 0)
96 return false;
97 if (S_ISDIR(sb.st_mode))
98 return true;
99 return false;
100 }
101
102 static bool mkdir_p(const char *root, char *path)
103 {
104 char *b, orig, *e;
105
106 if (strlen(path) < strlen(root))
107 return false;
108 if (strlen(path) == strlen(root))
109 return true;
110
111 b = path + strlen(root) + 1;
112 while (1) {
113 while (*b && *b == '/')
114 b++;
115 if (!*b)
116 return true;
117 e = b + 1;
118 while (*e && *e != '/')
119 e++;
120 orig = *e;
121 if (orig)
122 *e = '\0';
123 if (exists(path))
124 goto next;
125 if (mkdir(path, 0755) < 0) {
126 #if DEBUG
127 fprintf(stderr, "Failed to create %s: %m\n", path);
128 #endif
129 return false;
130 }
131 next:
132 if (!orig)
133 return true;
134 *e = orig;
135 b = e + 1;
136 }
137
138 }
139
140 struct controller {
141 struct controller *next;
142 int id;
143 char *name;
144 char *mount_path;
145 char *init_path;
146 };
147
148 #define MAXCONTROLLERS 20
149 static struct controller *controllers[MAXCONTROLLERS];
150
151 /* Find the path at which each controller is mounted. */
152 static void get_mounted_paths(void)
153 {
154 int i;
155 struct controller *c;
156 char *path;
157
158 for (i = 0; i < MAXCONTROLLERS; i++) {
159 c = controllers[i];
160 if (!c || c->mount_path)
161 continue;
162 path = must_strcat("/sys/fs/cgroup/", c->name, NULL);
163 if (!exists(path)) {
164 free(path);
165 continue;
166 }
167 while (c) {
168 c->mount_path = path;
169 c = c->next;
170 }
171 }
172 }
173
174 static void add_controller(int id, char *tok)
175 {
176 struct controller *c;
177
178 do {
179 c = malloc(sizeof(struct controller));
180 } while (!c);
181 do {
182 c->name = strdup(tok);
183 } while (!c->name);
184 c->id = id;
185 c->next = controllers[id];
186 c->mount_path = NULL;
187 c->init_path = NULL;
188 controllers[id] = c;
189 }
190
191 static void drop_controller(int which)
192 {
193 struct controller *c = controllers[which];
194
195 if (c) {
196 free(c->init_path); // all comounts share this
197 free(c->mount_path);
198 }
199 while (c) {
200 struct controller *tmp = c->next;
201 free(c->name);
202 free(c);
203 c = tmp;
204 }
205 controllers[which] = NULL;
206 }
207
208 static bool single_in_filter(char *c, const char *filter)
209 {
210 char *dup = strdupa(filter), *tok;
211 for (tok = strtok(dup, ","); tok; tok = strtok(NULL, ",")) {
212 if (strcmp(c, tok) == 0)
213 return true;
214 }
215 return false;
216 }
217
218 static bool controller_in_filter(struct controller *controller, const char *filter)
219 {
220 struct controller *c;
221
222 for (c = controller; c; c = c->next) {
223 if (single_in_filter(c->name, filter))
224 return true;
225 }
226 return false;
227 }
228
229 /*
230 * Passed a comma-delimited list of requested controllers.
231 * Pulls any controllers not in the list out of the
232 * list of controllers
233 */
234 static void filter_controllers(const char *filter)
235 {
236 int i;
237 for (i = 0; i < MAXCONTROLLERS; i++) {
238 if (!controllers[i])
239 continue;
240 if (filter && !controller_in_filter(controllers[i], filter))
241 drop_controller(i);
242 }
243 }
244
245 #define INIT_SCOPE "/init.scope"
246 static void prune_init_scope(char *cg)
247 {
248 char *point;
249
250 if (!cg)
251 return;
252
253 point = cg + strlen(cg) - strlen(INIT_SCOPE);
254 if (point < cg)
255 return;
256 if (strcmp(point, INIT_SCOPE) == 0) {
257 if (point == cg)
258 *(point+1) = '\0';
259 else
260 *point = '\0';
261 }
262 }
263
264 static bool fill_in_init_paths(void)
265 {
266 FILE *f;
267 char *line = NULL;
268 size_t len = 0;
269 struct controller *c;
270
271 f = fopen("/proc/1/cgroup", "r");
272 if (!f)
273 return false;
274 while (getline(&line, &len, f) != -1) {
275 int id;
276 char *subsystems, *ip;
277 if (sscanf(line, "%d:%m[^:]:%ms", &id, &subsystems, &ip) != 3) {
278 mysyslog(LOG_ERR, "Corrupt /proc/1/cgroup\n");
279 fclose(f);
280 return false;
281 }
282 free(subsystems);
283 if (id < 0 || id > 20) {
284 mysyslog(LOG_ERR, "Too many subsystems\n");
285 free(ip);
286 fclose(f);
287 return false;
288 }
289 if (ip[0] != '/') {
290 free(ip);
291 mysyslog(LOG_ERR, "ERROR: init cgroup path is not absolute!\n");
292 return false;
293 }
294 prune_init_scope(ip);
295 for (c = controllers[id]; c; c = c->next)
296 c->init_path = ip;
297 }
298 fclose(f);
299 return true;
300 }
301
302 #if DEBUG
303 static void print_found_controllers(void) {
304 struct controller *c;
305 int i;
306
307 for (i = 0; i < MAXCONTROLLERS; i++) {
308 c = controllers[i];
309 if (!c) {
310 fprintf(stderr, "Nothing in controller %d\n", i);
311 continue;
312 }
313 fprintf(stderr, "Controller %d:\n", i);
314 while (c) {
315 fprintf(stderr, " Next mount: index %d name %s\n", c->id, c->name);
316 fprintf(stderr, " mount path %s\n", c->mount_path ? c->mount_path : "(none)");
317 fprintf(stderr, " init task path %s\n", c->init_path);
318 c = c->next;
319 }
320 }
321 }
322 #else
323 static inline void print_found_controllers(void) { };
324 #endif
325 /*
326 * Get the list of cgroup controllers currently mounted.
327 * This includes both kernel and named subsystems, so get the list from
328 * /proc/self/cgroup rather than /proc/cgroups.
329 */
330 static bool get_active_controllers(void)
331 {
332 FILE *f;
333 char *line = NULL, *tok;
334 size_t len = 0;
335
336 f = fopen("/proc/self/cgroup", "r");
337 if (!f)
338 return false;
339 while (getline(&line, &len, f) != -1) {
340 int id;
341 char *subsystems;
342 if (sscanf(line, "%d:%m[^:]:", &id, &subsystems) != 2) {
343 mysyslog(LOG_ERR, "Corrupt /proc/self/cgroup\n");
344 fclose(f);
345 return false;
346 }
347 if (id < 0 || id > 20) {
348 mysyslog(LOG_ERR, "Too many subsystems\n");
349 free(subsystems);
350 fclose(f);
351 return false;
352 }
353 if (strcmp(subsystems, "name=systemd") == 0)
354 goto next;
355 for (tok = strtok(subsystems, ","); tok; tok = strtok(NULL, ","))
356 add_controller(id, tok);
357 next:
358 free(subsystems);
359 }
360 fclose(f);
361
362 get_mounted_paths();
363
364 if (!fill_in_init_paths()) {
365 mysyslog(LOG_ERR, "Failed finding cgroups for init task\n");
366 return false;
367 }
368
369 print_found_controllers();
370
371 initialized = true;
372
373 return true;
374 }
375
376 static bool cgfs_create_forone(const struct controller *c, uid_t uid, gid_t gid, const char *cg, bool *existed)
377 {
378 while (c) {
379 if (!c->mount_path || !c->init_path)
380 goto next;
381 char *path = must_strcat(c->mount_path, c->init_path, cg, NULL);
382 #if DEBUG
383 fprintf(stderr, "Creating %s for %s\n", path, c->name);
384 #endif
385 if (exists(path)) {
386 free(path);
387 *existed = true;
388 #if DEBUG
389 fprintf(stderr, "%s existed\n", path);
390 #endif
391 return true;
392 }
393 bool pass = mkdir_p(c->mount_path, path);
394 #if DEBUG
395 fprintf(stderr, "Creating %s %s\n", path, pass ? "succeeded" : "failed");
396 #endif
397 if (pass) {
398 if (chown(path, uid, gid) < 0)
399 mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %m\n",
400 path, (int)uid, (int)gid);
401 }
402 free(path);
403 if (pass)
404 return true;
405 next:
406 c = c->next;
407 }
408 return false;
409 }
410
411 static void recursive_rmdir(const char *path)
412 {
413 struct dirent *direntp;
414 DIR *dir;
415
416 dir = opendir(path);
417 if (!dir)
418 return;
419 while ((direntp = readdir(dir))!= NULL) {
420 if (!strcmp(direntp->d_name, ".") ||
421 !strcmp(direntp->d_name, ".."))
422 continue;
423
424 char *dpath = must_strcat(path, "/", direntp->d_name, NULL);
425 if (is_dir(dpath)) {
426 recursive_rmdir(dpath);
427 #if DEBUG
428 fprintf(stderr, "attempting to remove %s\n", dpath);
429 #endif
430 if (rmdir(dpath) < 0) {
431 #if DEBUG
432 fprintf(stderr, "Failed removing %s: %m\n", dpath);
433 #endif
434 }
435 }
436 free(dpath);
437 }
438
439 closedir(dir);
440 }
441
442 /*
443 * Try to remove a cgroup in a controller to cleanup during failure.
444 * All mounts of comounted controllers are the same, so we just look
445 * for the first mount which exists, try to remove the directory, and
446 * return.
447 */
448 static void cgfs_remove_forone(int idx, const char *cg)
449 {
450 struct controller *c = controllers[idx];
451 char *path;
452
453 while (c) {
454 if (c->mount_path) {
455 path = must_strcat(c->mount_path, cg, NULL);
456 recursive_rmdir(path);
457 free(path);
458 }
459 c = c->next;
460 }
461 }
462
463 static bool cgfs_create(const char *cg, uid_t uid, gid_t gid, bool *existed)
464 {
465 *existed = false;
466 int i, j;
467
468 #if DEBUG
469 fprintf(stderr, "creating %s\n", cg);
470 #endif
471 for (i = 0; i < MAXCONTROLLERS; i++) {
472 struct controller *c = controllers[i];
473
474 if (!c)
475 continue;
476
477 if (!cgfs_create_forone(c, uid, gid, cg, existed)) {
478 for (j = 0; j < i; j++)
479 cgfs_remove_forone(j, cg);
480 return false;
481 }
482 }
483
484 return true;
485 }
486
487 static bool write_int(char *path, int v)
488 {
489 FILE *f = fopen(path, "w");
490 if (!f)
491 return false;
492 fprintf(f, "%d\n", v);
493 fclose(f);
494 return true;
495 }
496
497 static bool do_enter(struct controller *c, const char *cg)
498 {
499 char *path;
500 bool pass;
501
502 while (c) {
503 if (!c->mount_path || !c->init_path)
504 continue;
505 path = must_strcat(c->mount_path, c->init_path, cg, "/cgroup.procs", NULL);
506 if (!exists(path)) {
507 free(path);
508 path = must_strcat(c->mount_path, c->init_path, cg, "/tasks", NULL);
509 }
510 #if DEBUG
511 fprintf(stderr, "Attempting to enter %s:%s using %s\n", c->name, cg, path);
512 #endif
513 pass = write_int(path, (int)getpid());
514 free(path);
515 if (pass) /* only have to enter one of the comounts */
516 return true;
517 #if DEBUG
518 if (!pass)
519 fprintf(stderr, "Failed to enter %s:%s\n", c->name, cg);
520 #endif
521 c = c->next;
522 }
523
524 return false;
525 }
526
527 static bool cgfs_enter(const char *cg)
528 {
529 int i;
530
531 for (i = 0; i < MAXCONTROLLERS; i++) {
532 struct controller *c = controllers[i];
533
534 if (!c)
535 continue;
536
537 if (!do_enter(c, cg))
538 return false;
539 }
540
541 return true;
542 }
543
544 static void cgfs_escape(void)
545 {
546 if (!cgfs_enter("/")) {
547 mysyslog(LOG_WARNING, "Failed to escape to init's cgroup\n");
548 }
549 }
550
551 static bool get_uid_gid(const char *user, uid_t *uid, gid_t *gid)
552 {
553 struct passwd *pwent;
554
555 pwent = getpwnam(user);
556 if (!pwent)
557 return false;
558 *uid = pwent->pw_uid;
559 *gid = pwent->pw_gid;
560
561 return true;
562 }
563
564 #define DIRNAMSZ 200
565 static int handle_login(const char *user)
566 {
567 int idx = 0, ret;
568 bool existed;
569 uid_t uid = 0;
570 gid_t gid = 0;
571 char cg[MAXPATHLEN];
572
573 if (!get_uid_gid(user, &uid, &gid)) {
574 mysyslog(LOG_ERR, "Failed to get uid and gid for %s\n", user);
575 return PAM_SESSION_ERR;
576 }
577
578 cgfs_escape();
579
580 while (idx >= 0) {
581 ret = snprintf(cg, MAXPATHLEN, "/user/%s/%d", user, idx);
582 if (ret < 0 || ret >= MAXPATHLEN) {
583 mysyslog(LOG_ERR, "username too long\n");
584 return PAM_SESSION_ERR;
585 }
586
587 if (!cgfs_create(cg, uid, gid, &existed)) {
588 mysyslog(LOG_ERR, "Failed to create a cgroup for user %s\n", user);
589 return PAM_SESSION_ERR;
590 }
591
592 if (existed == 1) {
593 idx++;
594 continue;
595 }
596
597 if (!cgfs_enter(cg)) {
598 mysyslog(LOG_ERR, "Failed to enter user cgroup %s for user %s\n", cg, user);
599 return PAM_SESSION_ERR;
600 }
601 break;
602 }
603
604 return PAM_SUCCESS;
605 }
606
607 int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
608 const char **argv)
609 {
610 const char *PAM_user = NULL;
611 int ret;
612
613 if (!get_active_controllers()) {
614 mysyslog(LOG_ERR, "Failed to get list of controllers\n");
615 return PAM_SESSION_ERR;
616 }
617
618 if (argc > 1 && strcmp(argv[0], "-c") == 0)
619 filter_controllers(argv[1]);
620
621 ret = pam_get_user(pamh, &PAM_user, NULL);
622 if (ret != PAM_SUCCESS) {
623 mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n");
624 return PAM_SESSION_ERR;
625 }
626
627 ret = handle_login(PAM_user);
628 return ret;
629 }
630
631 static void prune_empty_cgroups(struct controller *c, const char *user)
632 {
633 while (c) {
634 if (!c->mount_path || !c->init_path)
635 goto next;
636 char *path = must_strcat(c->mount_path, c->init_path, "user/", user, NULL);
637 #if DEBUG
638 fprintf(stderr, "Pruning %s\n", path);
639 #endif
640 recursive_rmdir(path);
641 next:
642 c = c->next;
643 }
644 }
645
646 /*
647 * Since we can't rely on kernel's autoremove, remove stale cgroups
648 * any time the user logs out.
649 */
650 static void prune_user_cgs(const char *user)
651 {
652 int i;
653
654 for (i = 0; i < MAXCONTROLLERS; i++)
655 prune_empty_cgroups(controllers[i], user);
656 }
657
658 int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
659 const char **argv)
660 {
661 const char *PAM_user = NULL;
662 int ret = pam_get_user(pamh, &PAM_user, NULL);
663
664 if (ret != PAM_SUCCESS) {
665 mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n");
666 return PAM_SESSION_ERR;
667 }
668
669 if (!initialized) {
670 get_active_controllers();
671 if (argc > 1 && strcmp(argv[0], "-c") == 0)
672 filter_controllers(argv[1]);
673 }
674
675 prune_user_cgs(PAM_user);
676 return PAM_SUCCESS;
677 }