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