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