]> git.proxmox.com Git - mirror_lxcfs.git/blob - pam/pam_cgfs.c
pam: detect write failure in write_int()
[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 /*
152 * if cpu and cpuacct are comounted, it's possible a mount
153 * exists for only one. Find it.
154 */
155 static char *find_controller_path(struct controller *c)
156 {
157 while (c) {
158 char *path = must_strcat("/sys/fs/cgroup/", c->name, NULL);
159 if (exists(path))
160 return path;
161 free(path);
162 c = c->next;
163 }
164 return NULL;
165 }
166
167 /* Find the path at which each controller is mounted. */
168 static void get_mounted_paths(void)
169 {
170 int i;
171 struct controller *c;
172 char *path;
173
174 for (i = 0; i < MAXCONTROLLERS; i++) {
175 c = controllers[i];
176 if (!c || c->mount_path)
177 continue;
178 path = find_controller_path(c);
179 if (!path)
180 continue;
181 while (c) {
182 c->mount_path = path;
183 c = c->next;
184 }
185 }
186 }
187
188 static void add_controller(int id, char *tok)
189 {
190 struct controller *c;
191
192 do {
193 c = malloc(sizeof(struct controller));
194 } while (!c);
195 do {
196 c->name = strdup(tok);
197 } while (!c->name);
198 c->id = id;
199 c->next = controllers[id];
200 c->mount_path = NULL;
201 c->init_path = NULL;
202 controllers[id] = c;
203 }
204
205 static void drop_controller(int which)
206 {
207 struct controller *c = controllers[which];
208
209 if (c) {
210 free(c->init_path); // all comounts share this
211 free(c->mount_path);
212 }
213 while (c) {
214 struct controller *tmp = c->next;
215 free(c->name);
216 free(c);
217 c = tmp;
218 }
219 controllers[which] = NULL;
220 }
221
222 static bool single_in_filter(char *c, const char *filter)
223 {
224 char *dup = strdupa(filter), *tok;
225 for (tok = strtok(dup, ","); tok; tok = strtok(NULL, ",")) {
226 if (strcmp(c, tok) == 0)
227 return true;
228 }
229 return false;
230 }
231
232 static bool controller_in_filter(struct controller *controller, const char *filter)
233 {
234 struct controller *c;
235
236 for (c = controller; c; c = c->next) {
237 if (single_in_filter(c->name, filter))
238 return true;
239 }
240 return false;
241 }
242
243 /*
244 * Passed a comma-delimited list of requested controllers.
245 * Pulls any controllers not in the list out of the
246 * list of controllers
247 */
248 static void filter_controllers(const char *filter)
249 {
250 int i;
251 for (i = 0; i < MAXCONTROLLERS; i++) {
252 if (!controllers[i])
253 continue;
254 if (filter && !controller_in_filter(controllers[i], filter))
255 drop_controller(i);
256 }
257 }
258
259 #define INIT_SCOPE "/init.scope"
260 static void prune_init_scope(char *cg)
261 {
262 char *point;
263
264 if (!cg)
265 return;
266
267 point = cg + strlen(cg) - strlen(INIT_SCOPE);
268 if (point < cg)
269 return;
270 if (strcmp(point, INIT_SCOPE) == 0) {
271 if (point == cg)
272 *(point+1) = '\0';
273 else
274 *point = '\0';
275 }
276 }
277
278 static bool fill_in_init_paths(void)
279 {
280 FILE *f;
281 char *line = NULL;
282 size_t len = 0;
283 struct controller *c;
284 bool ret = false;
285
286 f = fopen("/proc/1/cgroup", "r");
287 if (!f)
288 return false;
289 while (getline(&line, &len, f) != -1) {
290 int id;
291 char *subsystems, *ip;
292 if (sscanf(line, "%d:%m[^:]:%ms", &id, &subsystems, &ip) != 3) {
293 mysyslog(LOG_ERR, "Corrupt /proc/1/cgroup\n");
294 goto out;
295 }
296 free(subsystems);
297 if (id < 0 || id > 20) {
298 mysyslog(LOG_ERR, "Too many subsystems\n");
299 free(ip);
300 goto out;
301 }
302 if (ip[0] != '/') {
303 free(ip);
304 mysyslog(LOG_ERR, "ERROR: init cgroup path is not absolute!\n");
305 goto out;
306 }
307 prune_init_scope(ip);
308 for (c = controllers[id]; c; c = c->next)
309 c->init_path = ip;
310 }
311 ret = true;
312 out:
313 fclose(f);
314 free(line);
315 return ret;
316 }
317
318 #if DEBUG
319 static void print_found_controllers(void) {
320 struct controller *c;
321 int i;
322
323 for (i = 0; i < MAXCONTROLLERS; i++) {
324 c = controllers[i];
325 if (!c) {
326 fprintf(stderr, "Nothing in controller %d\n", i);
327 continue;
328 }
329 fprintf(stderr, "Controller %d:\n", i);
330 while (c) {
331 fprintf(stderr, " Next mount: index %d name %s\n", c->id, c->name);
332 fprintf(stderr, " mount path %s\n", c->mount_path ? c->mount_path : "(none)");
333 fprintf(stderr, " init task path %s\n", c->init_path);
334 c = c->next;
335 }
336 }
337 }
338 #else
339 static inline void print_found_controllers(void) { };
340 #endif
341 /*
342 * Get the list of cgroup controllers currently mounted.
343 * This includes both kernel and named subsystems, so get the list from
344 * /proc/self/cgroup rather than /proc/cgroups.
345 */
346 static bool get_active_controllers(void)
347 {
348 FILE *f;
349 char *line = NULL, *tok;
350 size_t len = 0;
351
352 f = fopen("/proc/self/cgroup", "r");
353 if (!f)
354 return false;
355 while (getline(&line, &len, f) != -1) {
356 int id;
357 char *subsystems;
358 if (sscanf(line, "%d:%m[^:]:", &id, &subsystems) != 2) {
359 mysyslog(LOG_ERR, "Corrupt /proc/self/cgroup\n");
360 fclose(f);
361 free(line);
362 return false;
363 }
364 if (id < 0 || id > 20) {
365 mysyslog(LOG_ERR, "Too many subsystems\n");
366 free(subsystems);
367 fclose(f);
368 free(line);
369 return false;
370 }
371 if (strcmp(subsystems, "name=systemd") == 0)
372 goto next;
373 for (tok = strtok(subsystems, ","); tok; tok = strtok(NULL, ","))
374 add_controller(id, tok);
375 next:
376 free(subsystems);
377 }
378 fclose(f);
379 free(line);
380
381 get_mounted_paths();
382
383 if (!fill_in_init_paths()) {
384 mysyslog(LOG_ERR, "Failed finding cgroups for init task\n");
385 return false;
386 }
387
388 print_found_controllers();
389
390 initialized = true;
391
392 return true;
393 }
394
395 static bool cgfs_create_forone(const struct controller *c, uid_t uid, gid_t gid, const char *cg, bool *existed)
396 {
397 while (c) {
398 if (!c->mount_path || !c->init_path)
399 goto next;
400 char *path = must_strcat(c->mount_path, c->init_path, cg, NULL);
401 #if DEBUG
402 fprintf(stderr, "Creating %s for %s\n", path, c->name);
403 #endif
404 if (exists(path)) {
405 free(path);
406 *existed = true;
407 #if DEBUG
408 fprintf(stderr, "%s existed\n", path);
409 #endif
410 return true;
411 }
412 bool pass = mkdir_p(c->mount_path, path);
413 #if DEBUG
414 fprintf(stderr, "Creating %s %s\n", path, pass ? "succeeded" : "failed");
415 #endif
416 if (pass) {
417 if (chown(path, uid, gid) < 0)
418 mysyslog(LOG_WARNING, "Failed to chown %s to %d:%d: %m\n",
419 path, (int)uid, (int)gid);
420 }
421 free(path);
422 if (pass)
423 return true;
424 next:
425 c = c->next;
426 }
427 return false;
428 }
429
430 static void recursive_rmdir(const char *path)
431 {
432 struct dirent *direntp;
433 DIR *dir;
434
435 dir = opendir(path);
436 if (!dir)
437 return;
438 while ((direntp = readdir(dir))!= NULL) {
439 if (!strcmp(direntp->d_name, ".") ||
440 !strcmp(direntp->d_name, ".."))
441 continue;
442
443 char *dpath = must_strcat(path, "/", direntp->d_name, NULL);
444 if (is_dir(dpath)) {
445 recursive_rmdir(dpath);
446 #if DEBUG
447 fprintf(stderr, "attempting to remove %s\n", dpath);
448 #endif
449 if (rmdir(dpath) < 0) {
450 #if DEBUG
451 fprintf(stderr, "Failed removing %s: %m\n", dpath);
452 #endif
453 }
454 }
455 free(dpath);
456 }
457
458 closedir(dir);
459 }
460
461 /*
462 * Try to remove a cgroup in a controller to cleanup during failure.
463 * All mounts of comounted controllers are the same, so we just look
464 * for the first mount which exists, try to remove the directory, and
465 * return.
466 */
467 static void cgfs_remove_forone(int idx, const char *cg)
468 {
469 struct controller *c = controllers[idx];
470 char *path;
471
472 while (c) {
473 if (c->mount_path) {
474 path = must_strcat(c->mount_path, cg, NULL);
475 recursive_rmdir(path);
476 free(path);
477 }
478 c = c->next;
479 }
480 }
481
482 static bool cgfs_create(const char *cg, uid_t uid, gid_t gid, bool *existed)
483 {
484 *existed = false;
485 int i, j;
486
487 #if DEBUG
488 fprintf(stderr, "creating %s\n", cg);
489 #endif
490 for (i = 0; i < MAXCONTROLLERS; i++) {
491 struct controller *c = controllers[i];
492
493 if (!c)
494 continue;
495
496 if (!cgfs_create_forone(c, uid, gid, cg, existed)) {
497 for (j = 0; j < i; j++)
498 cgfs_remove_forone(j, cg);
499 return false;
500 }
501 }
502
503 return true;
504 }
505
506 static bool write_int(char *path, int v)
507 {
508 FILE *f = fopen(path, "w");
509 bool ret = true;
510
511 if (!f)
512 return false;
513 if (fprintf(f, "%d\n", v) < 0)
514 ret = false;
515 if (fclose(f) != 0)
516 ret = false;
517 return ret;
518 }
519
520 static bool do_enter(struct controller *c, const char *cg)
521 {
522 char *path;
523 bool pass;
524
525 while (c) {
526 if (!c->mount_path || !c->init_path)
527 continue;
528 path = must_strcat(c->mount_path, c->init_path, cg, "/cgroup.procs", NULL);
529 if (!exists(path)) {
530 free(path);
531 path = must_strcat(c->mount_path, c->init_path, cg, "/tasks", NULL);
532 }
533 #if DEBUG
534 fprintf(stderr, "Attempting to enter %s:%s using %s\n", c->name, cg, path);
535 #endif
536 pass = write_int(path, (int)getpid());
537 free(path);
538 if (pass) /* only have to enter one of the comounts */
539 return true;
540 #if DEBUG
541 if (!pass)
542 fprintf(stderr, "Failed to enter %s:%s\n", c->name, cg);
543 #endif
544 c = c->next;
545 }
546
547 return false;
548 }
549
550 static bool cgfs_enter(const char *cg)
551 {
552 int i;
553
554 for (i = 0; i < MAXCONTROLLERS; i++) {
555 struct controller *c = controllers[i];
556
557 if (!c)
558 continue;
559
560 if (!do_enter(c, cg))
561 return false;
562 }
563
564 return true;
565 }
566
567 static void cgfs_escape(void)
568 {
569 if (!cgfs_enter("/")) {
570 mysyslog(LOG_WARNING, "Failed to escape to init's cgroup\n");
571 }
572 }
573
574 static bool get_uid_gid(const char *user, uid_t *uid, gid_t *gid)
575 {
576 struct passwd *pwent;
577
578 pwent = getpwnam(user);
579 if (!pwent)
580 return false;
581 *uid = pwent->pw_uid;
582 *gid = pwent->pw_gid;
583
584 return true;
585 }
586
587 #define DIRNAMSZ 200
588 static int handle_login(const char *user)
589 {
590 int idx = 0, ret;
591 bool existed;
592 uid_t uid = 0;
593 gid_t gid = 0;
594 char cg[MAXPATHLEN];
595
596 if (!get_uid_gid(user, &uid, &gid)) {
597 mysyslog(LOG_ERR, "Failed to get uid and gid for %s\n", user);
598 return PAM_SESSION_ERR;
599 }
600
601 cgfs_escape();
602
603 while (idx >= 0) {
604 ret = snprintf(cg, MAXPATHLEN, "/user/%s/%d", user, idx);
605 if (ret < 0 || ret >= MAXPATHLEN) {
606 mysyslog(LOG_ERR, "username too long\n");
607 return PAM_SESSION_ERR;
608 }
609
610 if (!cgfs_create(cg, uid, gid, &existed)) {
611 mysyslog(LOG_ERR, "Failed to create a cgroup for user %s\n", user);
612 return PAM_SESSION_ERR;
613 }
614
615 if (existed == 1) {
616 idx++;
617 continue;
618 }
619
620 if (!cgfs_enter(cg)) {
621 mysyslog(LOG_ERR, "Failed to enter user cgroup %s for user %s\n", cg, user);
622 return PAM_SESSION_ERR;
623 }
624 break;
625 }
626
627 return PAM_SUCCESS;
628 }
629
630 int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
631 const char **argv)
632 {
633 const char *PAM_user = NULL;
634 int ret;
635
636 if (!get_active_controllers()) {
637 mysyslog(LOG_ERR, "Failed to get list of controllers\n");
638 return PAM_SESSION_ERR;
639 }
640
641 if (argc > 1 && strcmp(argv[0], "-c") == 0)
642 filter_controllers(argv[1]);
643
644 ret = pam_get_user(pamh, &PAM_user, NULL);
645 if (ret != PAM_SUCCESS) {
646 mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n");
647 return PAM_SESSION_ERR;
648 }
649
650 ret = handle_login(PAM_user);
651 return ret;
652 }
653
654 static void prune_empty_cgroups(struct controller *c, const char *user)
655 {
656 while (c) {
657 if (!c->mount_path || !c->init_path)
658 goto next;
659 char *path = must_strcat(c->mount_path, c->init_path, "user/", user, NULL);
660 #if DEBUG
661 fprintf(stderr, "Pruning %s\n", path);
662 #endif
663 recursive_rmdir(path);
664 next:
665 c = c->next;
666 }
667 }
668
669 /*
670 * Since we can't rely on kernel's autoremove, remove stale cgroups
671 * any time the user logs out.
672 */
673 static void prune_user_cgs(const char *user)
674 {
675 int i;
676
677 for (i = 0; i < MAXCONTROLLERS; i++)
678 prune_empty_cgroups(controllers[i], user);
679 }
680
681 int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
682 const char **argv)
683 {
684 const char *PAM_user = NULL;
685 int ret = pam_get_user(pamh, &PAM_user, NULL);
686
687 if (ret != PAM_SUCCESS) {
688 mysyslog(LOG_ERR, "PAM-CGFS: couldn't get user\n");
689 return PAM_SESSION_ERR;
690 }
691
692 if (!initialized) {
693 get_active_controllers();
694 if (argc > 1 && strcmp(argv[0], "-c") == 0)
695 filter_controllers(argv[1]);
696 }
697
698 prune_user_cgs(PAM_user);
699 return PAM_SUCCESS;
700 }